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.
- package/README.md +53 -38
- package/dist/astro/client.cjs.js +18 -8106
- package/dist/astro/client.cjs.js.map +1 -0
- package/dist/astro/client.mjs +17 -8105
- package/dist/astro/client.mjs.map +1 -0
- package/dist/astro/index.cjs.js +768 -9
- package/dist/astro/index.cjs.js.map +1 -0
- package/dist/astro/index.mjs +768 -9
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/server.cjs.js +4 -1
- package/dist/astro/server.cjs.js.map +1 -0
- package/dist/astro/server.mjs +4 -1
- package/dist/astro/server.mjs.map +1 -0
- package/dist/diagnostics.cjs.js +1297 -0
- package/dist/diagnostics.cjs.js.map +1 -0
- package/dist/diagnostics.esm.js +1267 -0
- package/dist/diagnostics.esm.js.map +1 -0
- package/dist/index.cjs.js +2981 -1386
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +848 -111
- package/dist/index.esm.js +2997 -1411
- package/dist/index.esm.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs.js +15 -9
- package/dist/jsx-dev-runtime.cjs.js.map +1 -0
- package/dist/jsx-dev-runtime.esm.js +15 -9
- package/dist/jsx-dev-runtime.esm.js.map +1 -0
- package/dist/jsx-runtime.cjs.js +15 -9
- package/dist/jsx-runtime.cjs.js.map +1 -0
- package/dist/jsx-runtime.esm.js +15 -9
- package/dist/jsx-runtime.esm.js.map +1 -0
- package/dist/jsx.cjs.js +15 -9
- package/dist/jsx.cjs.js.map +1 -0
- package/dist/jsx.esm.js +15 -9
- package/dist/jsx.esm.js.map +1 -0
- package/dist/sygnal.min.js +2 -1
- package/dist/sygnal.min.js.map +1 -0
- package/dist/vike/+config.cjs.js +1 -0
- package/dist/vike/+config.cjs.js.map +1 -0
- package/dist/vike/+config.js +1 -0
- package/dist/vike/+config.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +1 -0
- package/dist/vike/ClientOnly.cjs.js.map +1 -0
- package/dist/vike/ClientOnly.mjs +1 -0
- package/dist/vike/ClientOnly.mjs.map +1 -0
- package/dist/vike/onRenderClient.cjs.js +1 -0
- package/dist/vike/onRenderClient.cjs.js.map +1 -0
- package/dist/vike/onRenderClient.mjs +1 -0
- package/dist/vike/onRenderClient.mjs.map +1 -0
- package/dist/vike/onRenderHtml.cjs.js +1 -0
- package/dist/vike/onRenderHtml.cjs.js.map +1 -0
- package/dist/vike/onRenderHtml.mjs +1 -0
- package/dist/vike/onRenderHtml.mjs.map +1 -0
- package/dist/vite/plugin.cjs.js +680 -47
- package/dist/vite/plugin.cjs.js.map +1 -0
- package/dist/vite/plugin.mjs +680 -47
- package/dist/vite/plugin.mjs.map +1 -0
- package/llms.txt +250 -0
- package/package.json +15 -6
- package/src/astro/client.ts +13 -2
- package/src/astro/index.d.ts +24 -1
- package/src/astro/index.ts +40 -9
- package/src/astro/server.ts +3 -1
- package/src/collection.ts +5 -2
- package/src/component.ts +375 -330
- package/src/cycle/dom/MainDOMSource.ts +4 -0
- package/src/cycle/dom/classNameModule.ts +74 -0
- package/src/cycle/dom/controlledInputModule.ts +33 -0
- package/src/cycle/dom/enrichEventStream.ts +9 -3
- package/src/cycle/dom/makeDOMDriver.ts +1 -2
- package/src/cycle/dom/mockDOMSource.ts +62 -8
- package/src/cycle/dom/modules.ts +5 -1
- package/src/cycle/state/Collection.ts +13 -3
- package/src/cycle/state/StateSource.ts +1 -1
- package/src/cycle/state/pickCombine.ts +35 -1
- package/src/cycle/state/withState.ts +1 -1
- package/src/extra/devtools.ts +8 -0
- package/src/extra/diagnostics/checks/collections.ts +67 -0
- package/src/extra/diagnostics/checks/dom.ts +241 -0
- package/src/extra/diagnostics/checks/events.ts +68 -0
- package/src/extra/diagnostics/checks/index.ts +109 -0
- package/src/extra/diagnostics/checks/inspect.ts +319 -0
- package/src/extra/diagnostics/checks/props.ts +51 -0
- package/src/extra/diagnostics/checks/public.d.ts +211 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
- package/src/extra/diagnostics/checks/shared.ts +119 -0
- package/src/extra/diagnostics/checks/state.ts +55 -0
- package/src/extra/diagnostics/checks/strict.ts +112 -0
- package/src/extra/diagnostics/checks/wiring.ts +65 -0
- package/src/extra/diagnostics/codes.ts +232 -0
- package/src/extra/diagnostics/index.ts +328 -0
- package/src/extra/diagnostics/legacy.ts +59 -0
- package/src/extra/driverFactories.ts +106 -58
- package/src/extra/eventDriver.ts +4 -0
- package/src/extra/flatten.ts +75 -0
- package/src/extra/reducers.ts +12 -5
- package/src/extra/run.ts +11 -0
- package/src/extra/testing.ts +890 -180
- package/src/extra/xstreamExtras.ts +269 -0
- package/src/index.d.ts +428 -17
- package/src/index.ts +7 -6
- package/src/pragma/index.ts +14 -9
- package/src/switchable.ts +10 -8
- package/src/vike/types.ts +21 -4
- package/src/vite/plugin.d.ts +95 -2
- package/src/vite/plugin.ts +730 -55
- package/dist/astro/astro/client.d.ts +0 -5
- package/dist/astro/astro/index.d.ts +0 -16
- package/dist/astro/astro/server.d.ts +0 -12
- package/dist/astro/client.d.ts +0 -5
- package/dist/astro/client.esm.js +0 -4665
- package/dist/astro/collection.d.ts +0 -12
- package/dist/astro/component.d.ts +0 -22
- package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/astro/cycle/dom/index.d.ts +0 -8
- package/dist/astro/cycle/dom/isolate.d.ts +0 -9
- package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/astro/cycle/dom/modules.d.ts +0 -5
- package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
- package/dist/astro/cycle/dom/thunk.d.ts +0 -11
- package/dist/astro/cycle/dom/utils.d.ts +0 -8
- package/dist/astro/cycle/isolate/index.d.ts +0 -42
- package/dist/astro/cycle/run/adapt.d.ts +0 -6
- package/dist/astro/cycle/run/index.d.ts +0 -36
- package/dist/astro/cycle/run/internals.d.ts +0 -8
- package/dist/astro/cycle/run/types.d.ts +0 -49
- package/dist/astro/cycle/state/Collection.d.ts +0 -29
- package/dist/astro/cycle/state/StateSource.d.ts +0 -20
- package/dist/astro/cycle/state/index.d.ts +0 -5
- package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
- package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
- package/dist/astro/cycle/state/types.d.ts +0 -18
- package/dist/astro/cycle/state/withState.d.ts +0 -17
- package/dist/astro/extra/classes.d.ts +0 -7
- package/dist/astro/extra/devtools.d.ts +0 -78
- package/dist/astro/extra/dragDriver.d.ts +0 -20
- package/dist/astro/extra/driverFactories.d.ts +0 -12
- package/dist/astro/extra/eventDriver.d.ts +0 -9
- package/dist/astro/extra/exactState.d.ts +0 -1
- package/dist/astro/extra/hmr.d.ts +0 -17
- package/dist/astro/extra/logDriver.d.ts +0 -2
- package/dist/astro/extra/processDrag.d.ts +0 -19
- package/dist/astro/extra/processForm.d.ts +0 -15
- package/dist/astro/extra/run.d.ts +0 -13
- package/dist/astro/extra/xstreamCompat.d.ts +0 -5
- package/dist/astro/index.d.ts +0 -19
- package/dist/astro/index.esm.js +0 -27
- package/dist/astro/jsx-dev-runtime.d.ts +0 -1
- package/dist/astro/jsx-runtime.d.ts +0 -4
- package/dist/astro/jsx.d.ts +0 -2
- package/dist/astro/pragma/fn.d.ts +0 -7
- package/dist/astro/pragma/index.d.ts +0 -7
- package/dist/astro/pragma/is.d.ts +0 -10
- package/dist/astro/server.d.ts +0 -12
- package/dist/astro/server.esm.js +0 -25
- package/dist/astro/switchable.d.ts +0 -7
- package/dist/collection.d.ts +0 -12
- package/dist/component.d.ts +0 -22
- package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/cycle/dom/index.d.ts +0 -8
- package/dist/cycle/dom/isolate.d.ts +0 -9
- package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/cycle/dom/modules.d.ts +0 -5
- package/dist/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/cycle/dom/styleModule.d.ts +0 -13
- package/dist/cycle/dom/thunk.d.ts +0 -11
- package/dist/cycle/dom/utils.d.ts +0 -8
- package/dist/cycle/isolate/index.d.ts +0 -42
- package/dist/cycle/run/adapt.d.ts +0 -6
- package/dist/cycle/run/index.d.ts +0 -36
- package/dist/cycle/run/internals.d.ts +0 -8
- package/dist/cycle/run/types.d.ts +0 -49
- package/dist/cycle/state/Collection.d.ts +0 -29
- package/dist/cycle/state/StateSource.d.ts +0 -20
- package/dist/cycle/state/index.d.ts +0 -5
- package/dist/cycle/state/pickCombine.d.ts +0 -3
- package/dist/cycle/state/pickMerge.d.ts +0 -3
- package/dist/cycle/state/types.d.ts +0 -18
- package/dist/cycle/state/withState.d.ts +0 -17
- package/dist/extra/classes.d.ts +0 -7
- package/dist/extra/devtools.d.ts +0 -78
- package/dist/extra/dragDriver.d.ts +0 -20
- package/dist/extra/driverFactories.d.ts +0 -12
- package/dist/extra/eventDriver.d.ts +0 -9
- package/dist/extra/exactState.d.ts +0 -1
- package/dist/extra/hmr.d.ts +0 -17
- package/dist/extra/logDriver.d.ts +0 -2
- package/dist/extra/processDrag.d.ts +0 -19
- package/dist/extra/processForm.d.ts +0 -15
- package/dist/extra/run.d.ts +0 -13
- package/dist/extra/xstreamCompat.d.ts +0 -5
- package/dist/jsx-dev-runtime.d.ts +0 -1
- package/dist/jsx-dev-runtime.js +0 -224
- package/dist/jsx-runtime.d.ts +0 -4
- package/dist/jsx-runtime.js +0 -224
- package/dist/jsx.d.ts +0 -2
- package/dist/pragma/fn.d.ts +0 -7
- package/dist/pragma/index.d.ts +0 -7
- package/dist/pragma/is.d.ts +0 -10
- 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()
|