sygnal 5.3.7 → 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 +670 -44
- package/dist/vite/plugin.cjs.js.map +1 -0
- package/dist/vite/plugin.mjs +670 -44
- 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 +720 -54
- 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,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inspect() — the machine-readable app graph at runtime (PLAN-1 workstream 2B).
|
|
3
|
+
*
|
|
4
|
+
* Built only from what the diagnostics hooks already see, keyed by component
|
|
5
|
+
* instance (so diagnostics must be on while the app runs):
|
|
6
|
+
* onIntent registers the instance (name, parent, kind) and its intent
|
|
7
|
+
* action names; claims the EVENTS.select() types seen since the
|
|
8
|
+
* previous onIntent (intent runs synchronously right before it)
|
|
9
|
+
* onModel action -> sinks; and wraps the component's own EVENTS model
|
|
10
|
+
* stream (component.model$.EVENTS, before initSinks merges it)
|
|
11
|
+
* in an identity map that records the emitted types. Since
|
|
12
|
+
* B-023 the driver-side emitter name (onBusEmit) is the
|
|
13
|
+
* emitting component's, but it is only a name, and this
|
|
14
|
+
* graph is per instance, so the per-instance tap stays.
|
|
15
|
+
* onReducer which model-only actions actually ran (=> dispatched by next())
|
|
16
|
+
* onBusEmit EVENTS types seen on the bus
|
|
17
|
+
* onBusSelect EVENTS types selected (see onIntent)
|
|
18
|
+
* onDispose prunes the instance
|
|
19
|
+
* Selectors come from the DOM check (real DOM) or from renderComponent (mock
|
|
20
|
+
* DOM, passed in as options.selectors). The same shape is produced statically
|
|
21
|
+
* by `sygnal-check --graph`; the JSON Schema is
|
|
22
|
+
* sygnal-check/schema/inspect.schema.json, the types are in ./public.d.ts.
|
|
23
|
+
*
|
|
24
|
+
* This lives in the 'sygnal/diagnostics' dev entry only (D29: zero bytes in
|
|
25
|
+
* app bundles). installInspect() publishes inspect() on the core bridge
|
|
26
|
+
* (renderComponent's t.inspect() reads it there) and on the devtools object
|
|
27
|
+
* (window.__SYGNAL_DEVTOOLS__, which is getDevTools()) once it exists.
|
|
28
|
+
*/
|
|
29
|
+
import type {DiagnosticCheck} from '../index'
|
|
30
|
+
import type {InspectGraph, InspectComponent, InspectOptions, InspectSelector, InspectDiagnostic, InspectChild} from './public'
|
|
31
|
+
import {bridge, onReset, nameOf, isPlainObject, BUILTIN_ACTIONS} from './shared'
|
|
32
|
+
import {checkEventBus} from './events'
|
|
33
|
+
import {selectorStatus} from './dom'
|
|
34
|
+
|
|
35
|
+
type Kind = InspectComponent['kind']
|
|
36
|
+
|
|
37
|
+
interface Rec {
|
|
38
|
+
instance: any
|
|
39
|
+
id: string
|
|
40
|
+
name: string
|
|
41
|
+
parentId: string | null
|
|
42
|
+
kind: Kind
|
|
43
|
+
/** null: single-stream intent (no action names) */
|
|
44
|
+
intentActions: string[] | null
|
|
45
|
+
injected: Set<string>
|
|
46
|
+
modelMap: Record<string, string[]>
|
|
47
|
+
fired: Set<string>
|
|
48
|
+
eventsSelected: Set<string>
|
|
49
|
+
eventsEmitted: Set<string>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let records = new Map<any, Rec>()
|
|
53
|
+
let pendingSelects: string[] = []
|
|
54
|
+
/** every EVENTS type seen on the bus */
|
|
55
|
+
let busTypes = new Set<string>()
|
|
56
|
+
|
|
57
|
+
onReset(() => {
|
|
58
|
+
records = new Map()
|
|
59
|
+
pendingSelects = []
|
|
60
|
+
busTypes = new Set()
|
|
61
|
+
})
|
|
62
|
+
const uniq = <T>(list: Iterable<T>): T[] => [...new Set(list)]
|
|
63
|
+
|
|
64
|
+
function kindOf(c: any): Kind {
|
|
65
|
+
const s = (c && c.sources) || {}
|
|
66
|
+
if (typeof s.__parentComponentNumber !== 'number') return 'root'
|
|
67
|
+
// Collection items get PARENT: null (src/component.ts instantiateCollection)
|
|
68
|
+
if ('PARENT' in s && s.PARENT === null) return 'collection-item'
|
|
69
|
+
// switchable() adds a lower-case `state` source next to the component's own (src/switchable.ts)
|
|
70
|
+
if (c.stateSourceName !== 'state' && 'state' in s && s.state !== s[c.stateSourceName]) return 'switchable'
|
|
71
|
+
return 'child'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ensure(c: any): Rec {
|
|
75
|
+
let r = records.get(c)
|
|
76
|
+
if (!r) {
|
|
77
|
+
const parent = c && c.sources && c.sources.__parentComponentNumber
|
|
78
|
+
r = {
|
|
79
|
+
instance: c,
|
|
80
|
+
id: String(c && c._componentNumber),
|
|
81
|
+
name: nameOf(c),
|
|
82
|
+
parentId: typeof parent === 'number' ? String(parent) : null,
|
|
83
|
+
kind: kindOf(c),
|
|
84
|
+
intentActions: [],
|
|
85
|
+
injected: new Set(),
|
|
86
|
+
modelMap: {},
|
|
87
|
+
fired: new Set(),
|
|
88
|
+
eventsSelected: new Set(),
|
|
89
|
+
eventsEmitted: new Set(),
|
|
90
|
+
}
|
|
91
|
+
records.set(c, r)
|
|
92
|
+
}
|
|
93
|
+
return r
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let devtools: any
|
|
97
|
+
function attachDevtools(): void {
|
|
98
|
+
const dt = (globalThis as any).__SYGNAL_DEVTOOLS__
|
|
99
|
+
if (!dt || typeof dt !== 'object') return
|
|
100
|
+
devtools = dt
|
|
101
|
+
if (dt.inspect !== inspect) {
|
|
102
|
+
try { dt.inspect = inspect } catch (_) { /* frozen: ignore */ }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const inspectCheck: DiagnosticCheck = {
|
|
107
|
+
id: 'inspect',
|
|
108
|
+
|
|
109
|
+
onBusSelect(type) {
|
|
110
|
+
if (type === undefined || type === null || type === '') pendingSelects.push('*')
|
|
111
|
+
else for (const t of ([] as any[]).concat(type)) if (typeof t === 'string') pendingSelects.push(t)
|
|
112
|
+
if (pendingSelects.length > 1000) pendingSelects.shift()
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
onIntent(component, actionNames) {
|
|
116
|
+
attachDevtools()
|
|
117
|
+
const r = ensure(component)
|
|
118
|
+
const intent$ = component && component.intent$
|
|
119
|
+
r.intentActions = intent$ && typeof intent$.addListener === 'function' ? null : (actionNames || [])
|
|
120
|
+
r.injected = new Set((intent$ && typeof intent$ === 'object' && intent$.__sygnalTestActions) || [])
|
|
121
|
+
for (const t of pendingSelects) r.eventsSelected.add(t)
|
|
122
|
+
pendingSelects = []
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
onModel(component, modelMap) {
|
|
126
|
+
const r = ensure(component)
|
|
127
|
+
r.modelMap = modelMap || {}
|
|
128
|
+
const model$ = component && component.model$
|
|
129
|
+
const events$ = model$ && model$.EVENTS
|
|
130
|
+
if (events$ && typeof events$.map === 'function' && !events$.__sygnalInspect) {
|
|
131
|
+
const tapped = events$.map((ev: any) => {
|
|
132
|
+
if (ev && typeof ev.type === 'string') r.eventsEmitted.add(ev.type)
|
|
133
|
+
return ev
|
|
134
|
+
})
|
|
135
|
+
tapped.__sygnalInspect = true
|
|
136
|
+
model$.EVENTS = tapped
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
onReducer(component, action) {
|
|
141
|
+
const r = records.get(component)
|
|
142
|
+
if (r && typeof action === 'string') r.fired.add(action)
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
onBusEmit(type) {
|
|
146
|
+
if (typeof type === 'string') busTypes.add(type)
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
onDispose(component) {
|
|
150
|
+
records.delete(component)
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const slim = (d: any): InspectDiagnostic => {
|
|
155
|
+
const out: InspectDiagnostic = {code: d.code, severity: d.severity, message: d.message}
|
|
156
|
+
if (d.component) out.component = d.component
|
|
157
|
+
if (d.fix) out.fix = d.fix
|
|
158
|
+
if (d.docsUrl) out.docsUrl = d.docsUrl
|
|
159
|
+
if (d.data !== undefined) {
|
|
160
|
+
// keep it JSON-safe (data can hold errors, vnodes, ...)
|
|
161
|
+
try { out.data = JSON.parse(JSON.stringify(d.data)) } catch (_) { /* drop */ }
|
|
162
|
+
}
|
|
163
|
+
return out
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function actionsOf(r: Rec): InspectComponent['actions'] {
|
|
167
|
+
const intent = (r.intentActions || []).filter(a => !r.injected.has(a))
|
|
168
|
+
const names = uniq([...intent, ...Object.keys(r.modelMap)]).filter(a => typeof a === 'string' && !a.startsWith('__'))
|
|
169
|
+
return names.map(name => ({
|
|
170
|
+
name,
|
|
171
|
+
trigger: BUILTIN_ACTIONS.has(name) ? 'builtin'
|
|
172
|
+
: intent.includes(name) ? 'intent'
|
|
173
|
+
: !r.injected.has(name) && r.fired.has(name) ? 'next'
|
|
174
|
+
: 'unknown',
|
|
175
|
+
sinks: [...(r.modelMap[name] || [])],
|
|
176
|
+
}))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function stateKeysOf(c: any, calculated: string[]): string[] {
|
|
180
|
+
const s = c && c.currentState
|
|
181
|
+
return isPlainObject(s) ? Object.keys(s).filter(k => !calculated.includes(k)) : []
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* renderComponent's mock DOM (src/extra/testing.ts passes its listener registry):
|
|
186
|
+
* listener paths are the select() chain with isolation scopes as '.___<scope>' entries;
|
|
187
|
+
* probe(selectors, scope) matches one against the latest vtree: own = in the component's
|
|
188
|
+
* own scope, child = the scope of the first (isolated) child component it matches inside.
|
|
189
|
+
*/
|
|
190
|
+
interface MockDom {
|
|
191
|
+
listeners: Map<string, string[]>
|
|
192
|
+
evTypes: Record<string, string[]>
|
|
193
|
+
owners: Map<string, string>
|
|
194
|
+
scopeIds: Map<string, number>
|
|
195
|
+
probe: (selectors: string[], scope?: string) => {own: boolean; child?: string}
|
|
196
|
+
vtree: any
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const isScope = (s: string) => s.startsWith('.___')
|
|
200
|
+
|
|
201
|
+
function mockSelectors(mock: MockDom): Record<string, InspectSelector[]> {
|
|
202
|
+
const out: Record<string, InspectSelector[]> = {}
|
|
203
|
+
mock.listeners.forEach((path, key) => {
|
|
204
|
+
const scope = path.filter(isScope).pop()
|
|
205
|
+
const id = mock.scopeIds.get(scope || '')
|
|
206
|
+
if (id === undefined) return
|
|
207
|
+
const sels = path.filter(s => !isScope(s)).join(' ').split(/[\s>]+/).filter(Boolean)
|
|
208
|
+
const page = !sels.length || /^(document|body)$/.test(sels[0])
|
|
209
|
+
const {own, child} = mock.vtree && !page ? mock.probe(sels, scope) : {own: false, child: undefined}
|
|
210
|
+
;(out[id] = out[id] || []).push({
|
|
211
|
+
selector: sels.join(' '),
|
|
212
|
+
events: uniq(mock.evTypes[key] || []),
|
|
213
|
+
matched: mock.vtree && !page ? own : null,
|
|
214
|
+
isolationHit: !own && child ? mock.owners.get(child) || 'a child component' : null,
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
return out
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function selectorsOf(r: Rec, options: InspectOptions, diags: any[]): InspectSelector[] {
|
|
221
|
+
const given = options.selectors && options.selectors[r.id]
|
|
222
|
+
if (given) return given.map(s => ({...s}))
|
|
223
|
+
return (selectorStatus(r.instance) || []).map(({selector, matched, crossed}) => {
|
|
224
|
+
const hit = diags.find(d => d.code === 'SYG104' && d.component === r.name && d.data && d.data.selector === selector)
|
|
225
|
+
const missed = diags.some(d => d.code === 'SYG103' && d.component === r.name && d.data && d.data.selector === selector)
|
|
226
|
+
return {
|
|
227
|
+
selector,
|
|
228
|
+
events: null,
|
|
229
|
+
matched: matched === null && missed ? false : matched,
|
|
230
|
+
isolationHit: crossed ? ((hit && hit.data.child) || 'a child component') : null,
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function childrenOf(r: Rec, all: Rec[]): InspectChild[] {
|
|
236
|
+
const via: Record<Kind, InspectChild['via']> = {root: 'tag', child: 'tag', 'collection-item': 'collection', switchable: 'switchable'}
|
|
237
|
+
const out = new Map<string, InspectChild>()
|
|
238
|
+
for (const c of all) {
|
|
239
|
+
if (c.parentId !== r.id) continue
|
|
240
|
+
const key = `${c.name}\u0000${via[c.kind]}`
|
|
241
|
+
const entry = out.get(key)
|
|
242
|
+
if (entry) entry.count = (entry.count || 1) + 1
|
|
243
|
+
else out.set(key, {name: c.name, via: via[c.kind], count: 1})
|
|
244
|
+
}
|
|
245
|
+
return [...out.values()]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The app graph of the live component instances (see ./public.d.ts). */
|
|
249
|
+
export function inspect(options: InspectOptions = {}): InspectGraph {
|
|
250
|
+
// report SYG105 (info) for EVENTS types selected but never emitted so far
|
|
251
|
+
try { checkEventBus() } catch (_) { /* 'error' mode: info never throws; be safe */ }
|
|
252
|
+
|
|
253
|
+
let diags: any[] = options.diagnostics as any[] || []
|
|
254
|
+
if (!options.diagnostics) {
|
|
255
|
+
attachDevtools()
|
|
256
|
+
try { diags = (devtools && typeof devtools.getDiagnostics === 'function' && devtools.getDiagnostics()) || [] } catch (_) { diags = [] }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const mock: MockDom | undefined = (options as any).mock
|
|
260
|
+
if (mock) options = {...options, selectors: {...mockSelectors(mock), ...options.selectors}}
|
|
261
|
+
|
|
262
|
+
const all = [...records.values()]
|
|
263
|
+
const ids = options.ids && new Set(options.ids.map(String))
|
|
264
|
+
const recs = ids ? all.filter(r => ids.has(r.id)) : all
|
|
265
|
+
const names = new Set(recs.map(r => r.name))
|
|
266
|
+
|
|
267
|
+
const components: InspectComponent[] = recs.map(r => {
|
|
268
|
+
const c = r.instance
|
|
269
|
+
const calculated = c && c.calculated && typeof c.calculated === 'object' ? Object.keys(c.calculated) : []
|
|
270
|
+
return {
|
|
271
|
+
name: r.name,
|
|
272
|
+
id: r.id,
|
|
273
|
+
parentId: r.parentId,
|
|
274
|
+
kind: r.kind,
|
|
275
|
+
actions: actionsOf(r),
|
|
276
|
+
stateKeys: stateKeysOf(c, calculated),
|
|
277
|
+
calculated,
|
|
278
|
+
contextProvides: c && isPlainObject(c.context) ? Object.keys(c.context) : [],
|
|
279
|
+
contextConsumes: null,
|
|
280
|
+
eventsEmitted: [...r.eventsEmitted],
|
|
281
|
+
eventsSelected: [...r.eventsSelected],
|
|
282
|
+
children: childrenOf(r, all),
|
|
283
|
+
selectors: selectorsOf(r, options, diags),
|
|
284
|
+
diagnostics: diags.filter(d => d && d.component === r.name).map(slim),
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const events: InspectGraph['events'] = {}
|
|
289
|
+
const ev = (type: string) => events[type] || (events[type] = {emitters: [], selectors: []})
|
|
290
|
+
const add = (list: string[], name: string) => { if (!list.includes(name)) list.push(name) }
|
|
291
|
+
for (const c of components) {
|
|
292
|
+
for (const t of c.eventsEmitted) add(ev(t).emitters, c.name)
|
|
293
|
+
for (const t of c.eventsSelected) add(ev(t).selectors, c.name)
|
|
294
|
+
}
|
|
295
|
+
// emitted on the bus by something that isn't a listed component (e.g. a driver, a pruned component)
|
|
296
|
+
if (!ids) for (const t of busTypes) ev(t)
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
version: 1,
|
|
300
|
+
source: 'runtime',
|
|
301
|
+
components,
|
|
302
|
+
events,
|
|
303
|
+
diagnostics: diags.filter(d => d && (!d.component || !names.has(d.component))).map(slim),
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Publish inspect() on the core bridge and the devtools object. Returns an uninstall function. */
|
|
308
|
+
export function installInspect(): () => void {
|
|
309
|
+
const core = bridge()
|
|
310
|
+
if (core) core.inspect = inspect
|
|
311
|
+
attachDevtools()
|
|
312
|
+
return () => {
|
|
313
|
+
if (core && core.inspect === inspect) core.inspect = undefined
|
|
314
|
+
const dt = (globalThis as any).__SYGNAL_DEVTOOLS__
|
|
315
|
+
if (dt && dt.inspect === inspect) {
|
|
316
|
+
try { delete dt.inspect } catch (_) { /* ignore */ }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SYG106 — a prop passed by the parent is silently overwritten (warn).
|
|
3
|
+
*
|
|
4
|
+
* The view is called with { ...props, state, children, slots, context, peers },
|
|
5
|
+
* so a parent prop with one of those names never reaches the child. (Tracker
|
|
6
|
+
* G-007 item 2.) `state` is special: Sygnal reads it as the child's state
|
|
7
|
+
* lens (a state field name or a { get, set } object), so it is only reported
|
|
8
|
+
* when the value can't be a lens.
|
|
9
|
+
*
|
|
10
|
+
* Mechanism: onRender reads the component's current (sanitized) props.
|
|
11
|
+
* Reported once per component name and prop.
|
|
12
|
+
*/
|
|
13
|
+
import type {DiagnosticCheck} from '../index'
|
|
14
|
+
import {report, once, nameOf} from './shared'
|
|
15
|
+
|
|
16
|
+
const RESERVED = ['children', 'slots', 'context', 'peers']
|
|
17
|
+
|
|
18
|
+
export const propsCheck: DiagnosticCheck = {
|
|
19
|
+
id: 'props',
|
|
20
|
+
|
|
21
|
+
onRender(component) {
|
|
22
|
+
const props = component && component.currentProps
|
|
23
|
+
if (!props || typeof props !== 'object') return
|
|
24
|
+
const name = nameOf(component)
|
|
25
|
+
|
|
26
|
+
for (const key of RESERVED) {
|
|
27
|
+
if (!(key in props) || !once(`SYG106:${name}:${key}`)) continue
|
|
28
|
+
report('SYG106', {
|
|
29
|
+
component,
|
|
30
|
+
severity: 'warn',
|
|
31
|
+
message: `The prop '${key}' passed to ${name} is overwritten: '${key}' is a reserved view argument, so the child never sees the parent's value`,
|
|
32
|
+
fix: `Rename the prop (for example '${key}Value'), in the parent and in ${name}`,
|
|
33
|
+
data: {prop: key},
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if ('state' in props) {
|
|
38
|
+
const value = props.state
|
|
39
|
+
const isLens = typeof value === 'string' || (value && typeof value === 'object' && typeof value.get === 'function')
|
|
40
|
+
if (!isLens && value !== undefined && once(`SYG106:${name}:state`)) {
|
|
41
|
+
report('SYG106', {
|
|
42
|
+
component,
|
|
43
|
+
severity: 'warn',
|
|
44
|
+
message: `The prop 'state' passed to ${name} is not data: Sygnal reads it as the child's state lens (a state field name or a { get, set } object), and the view's 'state' argument is the child's own state`,
|
|
45
|
+
fix: `To give ${name} a slice of the parent state use state="fieldName"; to pass data, use another prop name (for example 'item')`,
|
|
46
|
+
data: {prop: 'state', valueType: typeof value},
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the 'sygnal/diagnostics' entry (runtime consistency checks).
|
|
3
|
+
*
|
|
4
|
+
* import 'sygnal/diagnostics' // registers all checks (side effect)
|
|
5
|
+
*
|
|
6
|
+
* Checks only run while diagnostics are on: run(App, drivers, { diagnostics: 'warn' }),
|
|
7
|
+
* or globalThis.__SYGNAL_DEV__ (set by the Vite plugin in dev).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type DiagnosticSeverity = 'error' | 'warn' | 'info'
|
|
11
|
+
|
|
12
|
+
export interface DiagnosticCodeInfo {
|
|
13
|
+
code: string
|
|
14
|
+
severity: DiagnosticSeverity
|
|
15
|
+
title: string
|
|
16
|
+
/** anchor on https://sygnal.js.org/reference/errors, e.g. 'syg101' */
|
|
17
|
+
docsSlug: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A runtime check, as registered with the diagnostics core. */
|
|
21
|
+
export interface DiagnosticCheck {
|
|
22
|
+
id: string
|
|
23
|
+
onIntent?: (component: any, actionNames: string[], selectorsUsed: string[] | undefined) => void
|
|
24
|
+
onModel?: (component: any, modelMap: Record<string, string[]>) => void
|
|
25
|
+
onRender?: (component: any, rootVnode: any) => void
|
|
26
|
+
onReducer?: (component: any, action: string, prevState: any, nextState: any, sinkName: string) => void
|
|
27
|
+
onDispose?: (component: any) => void
|
|
28
|
+
onSelector?: (domSource: any, selector: string) => void
|
|
29
|
+
onBusEmit?: (type: string, emitterName?: string) => void
|
|
30
|
+
onBusSelect?: (type: string | string[] | undefined) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Strict mode (canonical forms, SYG5xx): turn the runtime strict checks on
|
|
35
|
+
* (true), off (false), or back to the default (undefined → on only when
|
|
36
|
+
* globalThis.__SYGNAL_STRICT__ === true). Off by default. Diagnostics must
|
|
37
|
+
* also be on. Runtime rules: SYG501 (positional view args), SYG502 (reducer
|
|
38
|
+
* returned the unchanged state instead of ABORT), SYG504 ('ACTION | SINK'
|
|
39
|
+
* keys); `sygnal-check --strict` checks all of SYG501-507 statically.
|
|
40
|
+
* In tests: renderComponent(C, { strict: true }).
|
|
41
|
+
*/
|
|
42
|
+
export function configureStrict(on?: boolean): void
|
|
43
|
+
|
|
44
|
+
/** Whether the runtime strict checks are on. */
|
|
45
|
+
export function isStrictEnabled(): boolean
|
|
46
|
+
|
|
47
|
+
/** Every runtime check in this entry. */
|
|
48
|
+
export const checks: DiagnosticCheck[]
|
|
49
|
+
|
|
50
|
+
/** Re-register all checks and the RxJS hints (done automatically on import). Returns an uninstall function. */
|
|
51
|
+
export function installChecks(): () => void
|
|
52
|
+
|
|
53
|
+
export interface ChecksOptions {
|
|
54
|
+
/** delay after a render before the DOM checks (SYG103/104) run; default 50 ms */
|
|
55
|
+
settleMs?: number
|
|
56
|
+
/** render-idle time before SYG103 escalates to warn; default 2000 ms */
|
|
57
|
+
idleMs?: number
|
|
58
|
+
/** renders required before SYG103 escalates to warn; default 3 */
|
|
59
|
+
minRenders?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Tune the timing of the DOM checks (mostly for tests). */
|
|
63
|
+
export function configureChecks(options?: ChecksOptions): void
|
|
64
|
+
|
|
65
|
+
/** Forget what the checks have seen and reported (dedupe state, registries). For tests. */
|
|
66
|
+
export function resetChecks(): void
|
|
67
|
+
|
|
68
|
+
export interface EventBusSummary {
|
|
69
|
+
selected: string[]
|
|
70
|
+
emitted: string[]
|
|
71
|
+
selectedNeverEmitted: string[]
|
|
72
|
+
emittedNeverSelected: string[]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Report SYG105 (info) for every EVENTS type selected but never emitted so far,
|
|
77
|
+
* and return the EVENTS bus registry.
|
|
78
|
+
*/
|
|
79
|
+
export function checkEventBus(): EventBusSummary
|
|
80
|
+
|
|
81
|
+
/** RxJS operator name -> xstream equivalent (used by SYG301). */
|
|
82
|
+
export const RXJS_HINTS: Record<string, string>
|
|
83
|
+
|
|
84
|
+
/** Metadata for every registered diagnostic code. */
|
|
85
|
+
export function listCodes(): DiagnosticCodeInfo[]
|
|
86
|
+
|
|
87
|
+
/** Metadata for one diagnostic code, or undefined if unknown. */
|
|
88
|
+
export function getCodeInfo(code: string): DiagnosticCodeInfo | undefined
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// inspect (PLAN-1 workstream 2B): a machine-readable app graph. The same shape
|
|
92
|
+
// is produced at runtime (inspect(), getDevTools().inspect(), renderComponent's
|
|
93
|
+
// t.inspect()) and statically (`sygnal-check --graph --json`); JSON Schema:
|
|
94
|
+
// sygnal-check/schema/inspect.schema.json. Fields one side cannot know are
|
|
95
|
+
// null (or omitted).
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/** How an action is dispatched. */
|
|
99
|
+
export type InspectActionTrigger = 'intent' | 'next' | 'builtin' | 'unknown'
|
|
100
|
+
|
|
101
|
+
export interface InspectAction {
|
|
102
|
+
name: string
|
|
103
|
+
/**
|
|
104
|
+
* 'intent': returned by the component's intent. 'builtin': BOOTSTRAP,
|
|
105
|
+
* INITIALIZE, HYDRATE, DISPOSE or READY. 'next': dispatched with next()
|
|
106
|
+
* (statically: a next('NAME') literal; at runtime: a model-only action whose
|
|
107
|
+
* STATE reducer was seen running). 'unknown': none of these is known.
|
|
108
|
+
*/
|
|
109
|
+
trigger: InspectActionTrigger
|
|
110
|
+
/** sinks of the model entry (STATE, EVENTS, EFFECT, PARENT, custom drivers); [] without a model entry */
|
|
111
|
+
sinks: string[]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface InspectChild {
|
|
115
|
+
name: string
|
|
116
|
+
via: 'tag' | 'collection' | 'switchable' | 'slot'
|
|
117
|
+
/** Collection: the `from` state field, when known */
|
|
118
|
+
from?: string | null
|
|
119
|
+
/** runtime: number of live instances behind this entry (e.g. Collection items) */
|
|
120
|
+
count?: number
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface InspectSelector {
|
|
124
|
+
/** CSS selector passed to DOM.select() (chained selects joined with a space) */
|
|
125
|
+
selector: string
|
|
126
|
+
/** event types listened to; null when unknown (runtime, real DOM) */
|
|
127
|
+
events: string[] | null
|
|
128
|
+
/**
|
|
129
|
+
* whether it matches an element the component itself renders; null when unknown.
|
|
130
|
+
* static: the class/id is in the view source. renderComponent: an element of the latest
|
|
131
|
+
* render matches (a conditionally rendered element that is hidden right now gives false).
|
|
132
|
+
* real DOM: true once a DOM check saw it match, false after SYG103/SYG104, else null.
|
|
133
|
+
*/
|
|
134
|
+
matched: boolean | null
|
|
135
|
+
/** the child component whose (isolated) elements it matches instead, if any */
|
|
136
|
+
isolationHit: string | null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface InspectDiagnostic {
|
|
140
|
+
code: string
|
|
141
|
+
severity: DiagnosticSeverity
|
|
142
|
+
component?: string
|
|
143
|
+
message: string
|
|
144
|
+
fix?: string
|
|
145
|
+
docsUrl?: string
|
|
146
|
+
data?: any
|
|
147
|
+
/** static only */
|
|
148
|
+
file?: string
|
|
149
|
+
line?: number
|
|
150
|
+
column?: number
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface InspectComponent {
|
|
154
|
+
name: string
|
|
155
|
+
/** runtime: instance number (as a string); static: 'file:line' of the definition */
|
|
156
|
+
id: string
|
|
157
|
+
/** runtime: the parent instance's id; static: null (see the parents' `children`) */
|
|
158
|
+
parentId: string | null
|
|
159
|
+
/** static only: source file, relative to the working directory */
|
|
160
|
+
file?: string
|
|
161
|
+
/**
|
|
162
|
+
* runtime: how the instance was created; static: 'root' when no scanned
|
|
163
|
+
* component renders it, else how it is first rendered
|
|
164
|
+
*/
|
|
165
|
+
kind: 'root' | 'child' | 'collection-item' | 'switchable'
|
|
166
|
+
actions: InspectAction[]
|
|
167
|
+
/** state keys (runtime: current state; static: initialState) */
|
|
168
|
+
stateKeys: string[]
|
|
169
|
+
/** calculated field names */
|
|
170
|
+
calculated: string[]
|
|
171
|
+
/** context fields this component provides (Component.context) */
|
|
172
|
+
contextProvides: string[]
|
|
173
|
+
/** context fields its view reads (static only; null at runtime) */
|
|
174
|
+
contextConsumes?: string[] | null
|
|
175
|
+
/** EVENTS types this component emits */
|
|
176
|
+
eventsEmitted: string[]
|
|
177
|
+
/** EVENTS types this component selects ('*' = EVENTS.select() without a type) */
|
|
178
|
+
eventsSelected: string[]
|
|
179
|
+
children: InspectChild[]
|
|
180
|
+
selectors: InspectSelector[]
|
|
181
|
+
diagnostics: InspectDiagnostic[]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface InspectGraph {
|
|
185
|
+
version: 1
|
|
186
|
+
/** which implementation produced the graph */
|
|
187
|
+
source?: 'runtime' | 'static'
|
|
188
|
+
components: InspectComponent[]
|
|
189
|
+
/** EVENTS bus: type -> names of the components that emit / select it */
|
|
190
|
+
events: Record<string, { emitters: string[]; selectors: string[] }>
|
|
191
|
+
/** diagnostics not tied to a listed component */
|
|
192
|
+
diagnostics: InspectDiagnostic[]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface InspectOptions {
|
|
196
|
+
/** only these component instances (ids as in InspectComponent.id) */
|
|
197
|
+
ids?: Array<string | number>
|
|
198
|
+
/** selector details by component id (renderComponent passes its mock-DOM view) */
|
|
199
|
+
selectors?: Record<string, InspectSelector[]>
|
|
200
|
+
/** diagnostics to attach (default: the devtools' collected diagnostics, when available) */
|
|
201
|
+
diagnostics?: Array<{ code: string; severity: DiagnosticSeverity; component?: string; message: string; [key: string]: any }>
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The app graph of the live component instances, built from what the
|
|
206
|
+
* diagnostics hooks have seen (so diagnostics must be on while the app runs).
|
|
207
|
+
* Disposed components are pruned. Also available as `getDevTools().inspect()`
|
|
208
|
+
* / `window.__SYGNAL_DEVTOOLS__.inspect()` once the devtools bridge exists
|
|
209
|
+
* (run() in a browser), and as renderComponent's `t.inspect()`.
|
|
210
|
+
*/
|
|
211
|
+
export function inspect(options?: InspectOptions): InspectGraph
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SYG301 — an RxJS operator was called on an xstream stream (error).
|
|
3
|
+
*
|
|
4
|
+
* Without help this fails with a bare "x.switchMap is not a function" inside
|
|
5
|
+
* the intent function. Mechanism: when 'sygnal/diagnostics' is loaded, every
|
|
6
|
+
* RxJS operator name below that xstream's Stream does NOT already have is
|
|
7
|
+
* defined on Stream.prototype (non-enumerable) as a method that throws an
|
|
8
|
+
* enriched TypeError: same "stream.<op> is not a function" shape, plus the
|
|
9
|
+
* xstream equivalent and the SYG301 docs link. The error is also reported
|
|
10
|
+
* (collected / printed per the diagnostics mode; in 'error' mode report()
|
|
11
|
+
* throws the DiagnosticError itself). It works anywhere a stream is used
|
|
12
|
+
* (intent, peers, drivers), with no hook in component.ts.
|
|
13
|
+
*
|
|
14
|
+
* Caveat: `typeof stream.pipe === 'function'` is true while this entry is
|
|
15
|
+
* loaded (dev only).
|
|
16
|
+
*/
|
|
17
|
+
import {report} from './shared'
|
|
18
|
+
import {docsUrlFor} from '../codes'
|
|
19
|
+
|
|
20
|
+
/** RxJS operator -> xstream equivalent. */
|
|
21
|
+
export const RXJS_HINTS: Record<string, string> = {
|
|
22
|
+
pipe: `chain xstream operators directly (.map(...).filter(...)), or use .compose(operator) for extra operators`,
|
|
23
|
+
switchMap: `.map(x => inner$).flatten()`,
|
|
24
|
+
mergeMap: `.map(x => inner$).compose(flattenConcurrently) (import { flattenConcurrently } from 'sygnal')`,
|
|
25
|
+
flatMap: `.map(x => inner$).compose(flattenConcurrently) (import { flattenConcurrently } from 'sygnal')`,
|
|
26
|
+
concatMap: `.map(x => inner$).compose(flattenSequentially) (import { flattenSequentially } from 'sygnal')`,
|
|
27
|
+
exhaustMap: `.map(x => inner$).flatten() (xstream has no exhaustMap; ignore new values while busy with .filter())`,
|
|
28
|
+
switchAll: `.flatten()`,
|
|
29
|
+
mergeAll: `.compose(flattenConcurrently) (import { flattenConcurrently } from 'sygnal')`,
|
|
30
|
+
concatAll: `.compose(flattenSequentially) (import { flattenSequentially } from 'sygnal')`,
|
|
31
|
+
debounceTime: `.compose(debounce(ms)) (import { debounce } from 'sygnal')`,
|
|
32
|
+
debounce: `.compose(debounce(ms)) (import { debounce } from 'sygnal')`,
|
|
33
|
+
throttleTime: `.compose(throttle(ms)) (import { throttle } from 'sygnal')`,
|
|
34
|
+
throttle: `.compose(throttle(ms)) (import { throttle } from 'sygnal')`,
|
|
35
|
+
auditTime: `.compose(throttle(ms)) (import { throttle } from 'sygnal')`,
|
|
36
|
+
delay: `.compose(delay(ms)) (import { delay } from 'sygnal')`,
|
|
37
|
+
distinctUntilChanged: `.compose(dropRepeats()) (import { dropRepeats } from 'sygnal')`,
|
|
38
|
+
withLatestFrom: `.compose(sampleCombine(other$)) (import { sampleCombine } from 'sygnal')`,
|
|
39
|
+
combineLatest: `xs.combine(a$, b$) (import { xs } from 'sygnal')`,
|
|
40
|
+
combineLatestWith: `xs.combine(a$, b$) (import { xs } from 'sygnal')`,
|
|
41
|
+
merge: `xs.merge(a$, b$) (import { xs } from 'sygnal')`,
|
|
42
|
+
mergeWith: `xs.merge(a$, b$) (import { xs } from 'sygnal')`,
|
|
43
|
+
concat: `concat(a$, b$) (import { concat } from 'sygnal')`,
|
|
44
|
+
scan: `.fold((acc, x) => next, seed)`,
|
|
45
|
+
reduce: `.fold((acc, x) => next, seed).last()`,
|
|
46
|
+
skip: `.drop(n)`,
|
|
47
|
+
first: `.take(1)`,
|
|
48
|
+
takeUntil: `.endWhen(other$)`,
|
|
49
|
+
tap: `.debug(fn)`,
|
|
50
|
+
do: `.debug(fn)`,
|
|
51
|
+
catchError: `.replaceError(err => fallback$)`,
|
|
52
|
+
catch: `.replaceError(err => fallback$)`,
|
|
53
|
+
pluck: `.map(x => x.field)`,
|
|
54
|
+
share: `xstream streams are already shared (hot); use .remember() to replay the latest value`,
|
|
55
|
+
shareReplay: `.remember()`,
|
|
56
|
+
publishReplay: `.remember()`,
|
|
57
|
+
finalize: `.debug({ complete: fn }) or handle completion in the subscriber`,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SYG301_URL = docsUrlFor('SYG301')
|
|
61
|
+
|
|
62
|
+
function rxjsError(op: string): TypeError {
|
|
63
|
+
const hint = RXJS_HINTS[op]
|
|
64
|
+
const message = `stream.${op} is not a function: '${op}' is an RxJS operator, but Sygnal streams are xstream streams`
|
|
65
|
+
const fix = `Use ${hint}`
|
|
66
|
+
// In 'error' mode report() throws the DiagnosticError itself (we are about to throw anyway).
|
|
67
|
+
const d = report('SYG301', {message, fix, data: {operator: op, hint}})
|
|
68
|
+
const err: any = new TypeError(d ? d.text : `[Sygnal SYG301] ${message}. ${fix}. ${SYG301_URL}`)
|
|
69
|
+
err.diagnostic = d
|
|
70
|
+
return err
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Install the hints on xstream's Stream.prototype. Returns an uninstall function. */
|
|
74
|
+
export function installRxjsHints(proto: any): () => void {
|
|
75
|
+
const added: string[] = []
|
|
76
|
+
if (!proto) return () => {}
|
|
77
|
+
for (const op of Object.keys(RXJS_HINTS)) {
|
|
78
|
+
if (op in proto) continue
|
|
79
|
+
Object.defineProperty(proto, op, {
|
|
80
|
+
configurable: true,
|
|
81
|
+
writable: true,
|
|
82
|
+
enumerable: false,
|
|
83
|
+
value: function rxjsOperatorHint() { throw rxjsError(op) },
|
|
84
|
+
})
|
|
85
|
+
added.push(op)
|
|
86
|
+
}
|
|
87
|
+
return () => { for (const op of added) delete proto[op] }
|
|
88
|
+
}
|