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,1297 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ns = require('xstream');
|
|
4
|
+
require('sygnal');
|
|
5
|
+
|
|
6
|
+
function _interopNamespaceDefault(e) {
|
|
7
|
+
var n = Object.create(null);
|
|
8
|
+
if (e) {
|
|
9
|
+
Object.keys(e).forEach(function (k) {
|
|
10
|
+
if (k !== 'default') {
|
|
11
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return e[k]; }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var ns__namespace = /*#__PURE__*/_interopNamespaceDefault(ns);
|
|
24
|
+
|
|
25
|
+
const d = ns__namespace.default;
|
|
26
|
+
const xs$1 = d && typeof d.create === 'function' ? d : d && d.default && typeof d.default.create === 'function' ? d.default : ns__namespace;
|
|
27
|
+
|
|
28
|
+
var xstreamNamespace = /*#__PURE__*/Object.freeze({
|
|
29
|
+
__proto__: null,
|
|
30
|
+
MemoryStream: ns.MemoryStream,
|
|
31
|
+
NO: ns.NO,
|
|
32
|
+
NO_IL: ns.NO_IL,
|
|
33
|
+
Stream: ns.Stream,
|
|
34
|
+
default: xs$1
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function resolveInteropDefault(namespaceValue) {
|
|
38
|
+
if (namespaceValue &&
|
|
39
|
+
namespaceValue.default &&
|
|
40
|
+
namespaceValue.default.default &&
|
|
41
|
+
typeof namespaceValue.default.default === 'function') {
|
|
42
|
+
return namespaceValue.default.default;
|
|
43
|
+
}
|
|
44
|
+
if (namespaceValue && namespaceValue.default) {
|
|
45
|
+
return namespaceValue.default;
|
|
46
|
+
}
|
|
47
|
+
return namespaceValue;
|
|
48
|
+
}
|
|
49
|
+
function getXs(namespaceValue) {
|
|
50
|
+
const value = resolveInteropDefault(namespaceValue);
|
|
51
|
+
if (value &&
|
|
52
|
+
value.default &&
|
|
53
|
+
typeof value.default.create === 'function') {
|
|
54
|
+
return value.default;
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
const xs = getXs(xstreamNamespace);
|
|
59
|
+
const Stream = ns.Stream ||
|
|
60
|
+
(xs$1 && xs$1.Stream) ||
|
|
61
|
+
(xs && xs.Stream);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Shared plumbing for the runtime checks (PLAN-1 workstream 1A).
|
|
65
|
+
*
|
|
66
|
+
* The checks are bundled separately from the main 'sygnal' entry
|
|
67
|
+
* ('sygnal/diagnostics'). They must talk to the SAME diagnostics core
|
|
68
|
+
* instance the components use, so they never import the core's functions
|
|
69
|
+
* directly: `import '../index'` only makes sure the core is loaded (in the
|
|
70
|
+
* dist build that import is rewritten to the external 'sygnal' package, see
|
|
71
|
+
* rollup.config.mjs), and the functions are read from the bridge object the
|
|
72
|
+
* core publishes on globalThis.__SYGNAL_DIAGNOSTICS__.
|
|
73
|
+
*/
|
|
74
|
+
const bridge = () => globalThis.__SYGNAL_DIAGNOSTICS__;
|
|
75
|
+
const report = (code, details) => bridge().report(code, details);
|
|
76
|
+
/**
|
|
77
|
+
* report() for code that is NOT running inside a core hook (timers, wrapped
|
|
78
|
+
* methods): never throws synchronously; an 'error'-mode DiagnosticError is
|
|
79
|
+
* rethrown asynchronously, like the core does for hooks.
|
|
80
|
+
*/
|
|
81
|
+
const reportSafely = (code, details) => {
|
|
82
|
+
try {
|
|
83
|
+
return report(code, details);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
const raise = () => { throw err; };
|
|
87
|
+
typeof queueMicrotask === 'function' ? queueMicrotask(raise) : setTimeout(raise);
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/** Tunables (test seam: configureChecks()). */
|
|
92
|
+
const timing = {
|
|
93
|
+
/** delay after a render before DOM checks run (lets nested renders patch) */
|
|
94
|
+
settleMs: 50,
|
|
95
|
+
/** SYG103 escalates to warn after this much render-idle time ... */
|
|
96
|
+
idleMs: 2000,
|
|
97
|
+
/** ... and only once the component has rendered at least this often */
|
|
98
|
+
minRenders: 3,
|
|
99
|
+
};
|
|
100
|
+
/** Built-in actions Sygnal dispatches itself; never "missing" or "unreachable". */
|
|
101
|
+
const BUILTIN_ACTIONS = new Set(['BOOTSTRAP', 'INITIALIZE', 'HYDRATE', 'DISPOSE', 'READY']);
|
|
102
|
+
/** Synthetic/internal actions (`__TEST_ACTION__`, `__NOOP_ACTION__`, ...) and built-ins. */
|
|
103
|
+
const isInternalAction = (action) => typeof action !== 'string' || action.startsWith('__') || BUILTIN_ACTIONS.has(action);
|
|
104
|
+
let seen = new Set();
|
|
105
|
+
/** true the first time `key` is seen (dedupes reports, e.g. per component name). */
|
|
106
|
+
const once = (key) => (seen.has(key) ? false : (seen.add(key), true));
|
|
107
|
+
/**
|
|
108
|
+
* Forget only the once() dedupe set (G-051: renderComponent calls it through the core bridge
|
|
109
|
+
* when an outermost instance starts, so findings are reported again in every test). Unlike
|
|
110
|
+
* resetCheckState() it keeps the checks' registries, e.g. inspect()'s live records.
|
|
111
|
+
*/
|
|
112
|
+
const resetOnce = () => { seen = new Set(); };
|
|
113
|
+
{
|
|
114
|
+
const core = bridge();
|
|
115
|
+
if (core)
|
|
116
|
+
core.resetOnce = resetOnce;
|
|
117
|
+
}
|
|
118
|
+
const resetters = [];
|
|
119
|
+
const onReset = (fn) => { resetters.push(fn); };
|
|
120
|
+
/** Clear every check's dedupe/registry state (tests). */
|
|
121
|
+
function resetCheckState() {
|
|
122
|
+
seen = new Set();
|
|
123
|
+
resetters.forEach(fn => fn());
|
|
124
|
+
}
|
|
125
|
+
const nameOf = (component) => (component && component.name) || 'component';
|
|
126
|
+
const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value) &&
|
|
127
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
128
|
+
/** Levenshtein distance (small strings only). */
|
|
129
|
+
function distance(a, b) {
|
|
130
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
131
|
+
for (let i = 1; i <= a.length; i++) {
|
|
132
|
+
let prev = row[0];
|
|
133
|
+
row[0] = i;
|
|
134
|
+
for (let j = 1; j <= b.length; j++) {
|
|
135
|
+
const tmp = row[j];
|
|
136
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
137
|
+
prev = tmp;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return row[b.length];
|
|
141
|
+
}
|
|
142
|
+
/** Closest candidate to `name` (case-insensitive match or edit distance <= 2), if any. */
|
|
143
|
+
function suggest(name, candidates) {
|
|
144
|
+
let best;
|
|
145
|
+
let bestScore = Infinity;
|
|
146
|
+
for (const c of candidates) {
|
|
147
|
+
if (c === name)
|
|
148
|
+
continue;
|
|
149
|
+
const score = c.toLowerCase() === name.toLowerCase() ? 0 : distance(c, name);
|
|
150
|
+
if (score < bestScore) {
|
|
151
|
+
best = c;
|
|
152
|
+
bestScore = score;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return bestScore <= Math.min(2, Math.floor(name.length / 3)) ? best : undefined;
|
|
156
|
+
}
|
|
157
|
+
const didYouMean = (name, candidates) => {
|
|
158
|
+
const s = suggest(name, candidates);
|
|
159
|
+
return s ? ` (did you mean '${s}'?)` : '';
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const intentActions = new WeakMap();
|
|
163
|
+
const wiringCheck = {
|
|
164
|
+
id: 'wiring',
|
|
165
|
+
onIntent(component, actionNames) {
|
|
166
|
+
intentActions.set(component, actionNames || []);
|
|
167
|
+
},
|
|
168
|
+
onModel(component, modelMap) {
|
|
169
|
+
const name = nameOf(component);
|
|
170
|
+
const intent$ = component && component.intent$;
|
|
171
|
+
const injected = new Set((intent$ && typeof intent$ === 'object' && intent$.__sygnalTestActions) || []);
|
|
172
|
+
const actions = (intentActions.get(component) || []).filter(a => !injected.has(a));
|
|
173
|
+
intentActions.delete(component);
|
|
174
|
+
const modelActions = Object.keys(modelMap || {});
|
|
175
|
+
for (const action of actions) {
|
|
176
|
+
if (isInternalAction(action) || modelActions.includes(action))
|
|
177
|
+
continue;
|
|
178
|
+
if (!once(`SYG101:${name}:${action}`))
|
|
179
|
+
continue;
|
|
180
|
+
report('SYG101', {
|
|
181
|
+
component,
|
|
182
|
+
message: `Intent action '${action}' has no model entry, so it never does anything${didYouMean(action, modelActions)}`,
|
|
183
|
+
fix: `Add '${action}' to ${name}.model, or remove it from ${name}.intent`,
|
|
184
|
+
data: { action, modelActions },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// A single-stream intent (intent returns one action$ stream) has no
|
|
188
|
+
// action names to compare against.
|
|
189
|
+
if (intent$ && typeof intent$.addListener === 'function')
|
|
190
|
+
return;
|
|
191
|
+
const hmr = [].concat(component?.hmrActions || []);
|
|
192
|
+
for (const action of modelActions) {
|
|
193
|
+
if (isInternalAction(action) || actions.includes(action) || hmr.includes(action) || injected.has(action))
|
|
194
|
+
continue;
|
|
195
|
+
if (!once(`SYG102:${name}:${action}`))
|
|
196
|
+
continue;
|
|
197
|
+
report('SYG102', {
|
|
198
|
+
component,
|
|
199
|
+
message: `Model entry '${action}' has no intent action with that name${didYouMean(action, actions)}`,
|
|
200
|
+
fix: `Add '${action}' to ${name}.intent, or remove the model entry. If it is only dispatched with next('${action}'), ignore this`,
|
|
201
|
+
data: { action, intentActions: actions },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const SKIP = new Set(['INITIALIZE', 'HYDRATE']);
|
|
208
|
+
const stateCheck = {
|
|
209
|
+
id: 'state',
|
|
210
|
+
onReducer(component, action, prevState, nextState) {
|
|
211
|
+
if (typeof action !== 'string' || action.startsWith('__') || SKIP.has(action))
|
|
212
|
+
return;
|
|
213
|
+
const name = nameOf(component);
|
|
214
|
+
if (nextState === undefined) {
|
|
215
|
+
if (!once(`SYG202:${name}:${action}`))
|
|
216
|
+
return;
|
|
217
|
+
// Returning undefined from a Collection item's reducer is the documented way to remove
|
|
218
|
+
// the item, so only the root (where it wipes the whole app state) is a warning.
|
|
219
|
+
const sub = !!component?.isSubComponent;
|
|
220
|
+
report('SYG202', {
|
|
221
|
+
component,
|
|
222
|
+
severity: sub ? 'info' : 'warn',
|
|
223
|
+
message: `The STATE reducer for '${action}' returned undefined` +
|
|
224
|
+
(sub ? ' (this removes the item if the component is a Collection item)' : ''),
|
|
225
|
+
fix: `Return the new state, e.g. (state, data) => ({ ...state, ... }), or return ABORT to leave the state unchanged`,
|
|
226
|
+
data: { action },
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!isPlainObject(prevState) || !isPlainObject(nextState))
|
|
231
|
+
return;
|
|
232
|
+
const calculated = component?._calculatedFieldNames || null;
|
|
233
|
+
const dropped = Object.keys(prevState).filter(key => !(key in nextState) && prevState[key] !== undefined && !(calculated && calculated.has(key)));
|
|
234
|
+
if (dropped.length === 0 || !once(`SYG201:${name}:${action}`))
|
|
235
|
+
return;
|
|
236
|
+
const list = dropped.map(k => `'${k}'`).join(', ');
|
|
237
|
+
report('SYG201', {
|
|
238
|
+
component,
|
|
239
|
+
message: `The STATE reducer for '${action}' returned a state without ${list}, which ${dropped.length === 1 ? 'was' : 'were'} in the previous state (likely a missing ...state spread)`,
|
|
240
|
+
fix: `Spread the previous state: (state, data) => ({ ...state, ... }). If removing ${dropped.length === 1 ? 'the key' : 'these keys'} is intended, ignore this`,
|
|
241
|
+
data: { action, droppedKeys: dropped },
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
let selected = new Set();
|
|
247
|
+
let emitted = new Set();
|
|
248
|
+
let selectsAll = false;
|
|
249
|
+
onReset(() => { selected = new Set(); emitted = new Set(); selectsAll = false; });
|
|
250
|
+
const eventsCheck = {
|
|
251
|
+
id: 'events',
|
|
252
|
+
onBusSelect(type) {
|
|
253
|
+
if (type === undefined || type === null || type === '') {
|
|
254
|
+
selectsAll = true;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
for (const t of [].concat(type))
|
|
258
|
+
if (typeof t === 'string')
|
|
259
|
+
selected.add(t);
|
|
260
|
+
},
|
|
261
|
+
onBusEmit(type, emitterName) {
|
|
262
|
+
if (typeof type !== 'string')
|
|
263
|
+
return;
|
|
264
|
+
emitted.add(type);
|
|
265
|
+
if (selectsAll || selected.has(type) || !once(`SYG105:emit:${type}`))
|
|
266
|
+
return;
|
|
267
|
+
report('SYG105', {
|
|
268
|
+
component: emitterName,
|
|
269
|
+
message: `EVENTS event '${type}' was emitted, but nothing selects it, so it was dropped${didYouMean(type, selected)}`,
|
|
270
|
+
fix: `Subscribe with EVENTS.select('${type}') in the component that should react, or check the event name`,
|
|
271
|
+
data: { type, direction: 'emitted-not-selected', emitter: emitterName },
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Report SYG105 (info) for every EVENTS type that has been selected but never
|
|
277
|
+
* emitted so far, and return the bus registry. Intended for inspect tooling and
|
|
278
|
+
* tests, at a point where the session is "done".
|
|
279
|
+
*/
|
|
280
|
+
function checkEventBus() {
|
|
281
|
+
const selectedNeverEmitted = [...selected].filter(t => !emitted.has(t));
|
|
282
|
+
const emittedNeverSelected = selectsAll ? [] : [...emitted].filter(t => !selected.has(t));
|
|
283
|
+
for (const type of selectedNeverEmitted) {
|
|
284
|
+
if (!once(`SYG105:select:${type}`))
|
|
285
|
+
continue;
|
|
286
|
+
report('SYG105', {
|
|
287
|
+
message: `EVENTS.select('${type}') is used, but no component has emitted '${type}'${didYouMean(type, emitted)}`,
|
|
288
|
+
fix: `Emit it from a model entry, e.g. { EVENTS: event('${type}', ...) }, or check the event name`,
|
|
289
|
+
data: { type, direction: 'selected-not-emitted' },
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return { selected: [...selected], emitted: [...emitted], selectedNeverEmitted, emittedNeverSelected };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const RESERVED = ['children', 'slots', 'context', 'peers'];
|
|
296
|
+
const propsCheck = {
|
|
297
|
+
id: 'props',
|
|
298
|
+
onRender(component) {
|
|
299
|
+
const props = component && component.currentProps;
|
|
300
|
+
if (!props || typeof props !== 'object')
|
|
301
|
+
return;
|
|
302
|
+
const name = nameOf(component);
|
|
303
|
+
for (const key of RESERVED) {
|
|
304
|
+
if (!(key in props) || !once(`SYG106:${name}:${key}`))
|
|
305
|
+
continue;
|
|
306
|
+
report('SYG106', {
|
|
307
|
+
component,
|
|
308
|
+
severity: 'warn',
|
|
309
|
+
message: `The prop '${key}' passed to ${name} is overwritten: '${key}' is a reserved view argument, so the child never sees the parent's value`,
|
|
310
|
+
fix: `Rename the prop (for example '${key}Value'), in the parent and in ${name}`,
|
|
311
|
+
data: { prop: key },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if ('state' in props) {
|
|
315
|
+
const value = props.state;
|
|
316
|
+
const isLens = typeof value === 'string' || (value && typeof value === 'object' && typeof value.get === 'function');
|
|
317
|
+
if (!isLens && value !== undefined && once(`SYG106:${name}:state`)) {
|
|
318
|
+
report('SYG106', {
|
|
319
|
+
component,
|
|
320
|
+
severity: 'warn',
|
|
321
|
+
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`,
|
|
322
|
+
fix: `To give ${name} a slice of the parent state use state="fieldName"; to pass data, use another prop name (for example 'item')`,
|
|
323
|
+
data: { prop: 'state', valueType: typeof value },
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const describe = (v) => v === null ? 'null' : v === undefined ? 'undefined' : Array.isArray(v) ? 'an array' : typeof v === 'object' ? 'an object' : `a ${typeof v}`;
|
|
331
|
+
function checkCollection(component, el) {
|
|
332
|
+
const props = (el && el.data && el.data.props) || {};
|
|
333
|
+
const from = props.from;
|
|
334
|
+
const of = props.of;
|
|
335
|
+
const ofName = typeof of === 'function' ? (of.componentName || of.label || of.name || 'component') : String(of);
|
|
336
|
+
const name = nameOf(component);
|
|
337
|
+
let state = component.currentState;
|
|
338
|
+
try {
|
|
339
|
+
if (typeof component.addCalculated === 'function')
|
|
340
|
+
state = component.addCalculated(state);
|
|
341
|
+
}
|
|
342
|
+
catch (_) { }
|
|
343
|
+
let problem;
|
|
344
|
+
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`;
|
|
345
|
+
// Every defined `from` (string field, lens object, or an invalid value) is reported by
|
|
346
|
+
// the core itself (SYG401/SYG412, retrofitted in 1E) in every diagnostics mode (D22), so
|
|
347
|
+
// this check only covers a missing `from`.
|
|
348
|
+
if (from !== undefined)
|
|
349
|
+
return;
|
|
350
|
+
if (!Array.isArray(state) && !(state && Array.isArray(state.value))) {
|
|
351
|
+
problem = `has no 'from' prop, so it uses ${name}'s whole state, which is ${describe(state)}, not an array`;
|
|
352
|
+
}
|
|
353
|
+
if (!problem || !once(`SYG401:${name}:undefined`))
|
|
354
|
+
return;
|
|
355
|
+
reportSafely('SYG401', {
|
|
356
|
+
component,
|
|
357
|
+
message: `<Collection of={${ofName}}> ${problem}, so it renders no items`,
|
|
358
|
+
fix,
|
|
359
|
+
data: { from: 'undefined', of: ofName },
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const WRAPPED = '__sygnalDiagWrapped';
|
|
363
|
+
const collectionsCheck = {
|
|
364
|
+
id: 'collections',
|
|
365
|
+
onModel(component) {
|
|
366
|
+
const original = component && component.instantiateCollection;
|
|
367
|
+
if (typeof original !== 'function' || original[WRAPPED])
|
|
368
|
+
return;
|
|
369
|
+
const wrapped = function (el, ...rest) {
|
|
370
|
+
try {
|
|
371
|
+
checkCollection(this, el);
|
|
372
|
+
}
|
|
373
|
+
catch (_) { }
|
|
374
|
+
return original.call(this, el, ...rest);
|
|
375
|
+
};
|
|
376
|
+
wrapped[WRAPPED] = true;
|
|
377
|
+
component.instantiateCollection = wrapped;
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
function isEqualNamespace(a, b) {
|
|
382
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
for (let i = 0; i < a.length; i++) {
|
|
386
|
+
if (a[i].type !== b[i].type || a[i].scope !== b[i].scope) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
class ScopeChecker {
|
|
394
|
+
constructor(namespace, isolateModule) {
|
|
395
|
+
this.namespace = namespace;
|
|
396
|
+
this.isolateModule = isolateModule;
|
|
397
|
+
this._namespace = namespace.filter(n => n.type !== 'selector');
|
|
398
|
+
}
|
|
399
|
+
isDirectlyInScope(leaf) {
|
|
400
|
+
const namespace = this.isolateModule.getNamespace(leaf);
|
|
401
|
+
if (!namespace) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
if (this._namespace.length > namespace.length ||
|
|
405
|
+
!isEqualNamespace(this._namespace, namespace.slice(0, this._namespace.length))) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
for (let i = this._namespace.length; i < namespace.length; i++) {
|
|
409
|
+
if (namespace[i].type === 'total') {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const scopesOf = (ns) => ns.filter(s => s.type !== 'selector');
|
|
418
|
+
const keyOf = (scopes) => scopes.map(s => `${s.type[0]}:${s.scope}`).join('/');
|
|
419
|
+
let pending = [];
|
|
420
|
+
let tracked = new WeakMap();
|
|
421
|
+
/** isolation-scope key -> the component instance that owns it (latest wins) */
|
|
422
|
+
let owners = new Map();
|
|
423
|
+
const ownerKeys = new WeakMap();
|
|
424
|
+
let matched = new Set();
|
|
425
|
+
let viewTokens = new WeakMap();
|
|
426
|
+
onReset(() => {
|
|
427
|
+
pending = [];
|
|
428
|
+
tracked = new WeakMap();
|
|
429
|
+
owners = new Map();
|
|
430
|
+
matched = new Set();
|
|
431
|
+
});
|
|
432
|
+
const unref = (t) => { if (t && typeof t.unref === 'function')
|
|
433
|
+
t.unref(); return t; };
|
|
434
|
+
function viewSource(component) {
|
|
435
|
+
const view = component && component.view;
|
|
436
|
+
if (typeof view !== 'function')
|
|
437
|
+
return '';
|
|
438
|
+
let src = viewTokens.get(view);
|
|
439
|
+
if (src === undefined) {
|
|
440
|
+
try {
|
|
441
|
+
src = Function.prototype.toString.call(view);
|
|
442
|
+
}
|
|
443
|
+
catch (_) {
|
|
444
|
+
src = '';
|
|
445
|
+
}
|
|
446
|
+
viewTokens.set(view, src);
|
|
447
|
+
}
|
|
448
|
+
return src;
|
|
449
|
+
}
|
|
450
|
+
/** class / id / tag tokens of a selector: '.a .b-c > #d' -> ['a', 'b-c', 'd'] */
|
|
451
|
+
const tokensOf = (selector) => (selector.replace(/\[[^\]]*\]|:[\w-]+(\([^)]*\))?/g, ' ').match(/[A-Za-z_][\w-]*/g) || []);
|
|
452
|
+
function appearsInView(component, selector) {
|
|
453
|
+
const src = viewSource(component);
|
|
454
|
+
if (!src)
|
|
455
|
+
return false;
|
|
456
|
+
const tokens = tokensOf(selector);
|
|
457
|
+
if (!tokens.length)
|
|
458
|
+
return false;
|
|
459
|
+
return tokens.every(t => new RegExp(`(^|[^\\w-])${t.replace(/[-]/g, '\\-')}($|[^\\w-])`).test(src));
|
|
460
|
+
}
|
|
461
|
+
function rootElementOf(t) {
|
|
462
|
+
try {
|
|
463
|
+
const im = t.source._isolateModule;
|
|
464
|
+
return im && im.getElement(scopesOf(t.source.namespace));
|
|
465
|
+
}
|
|
466
|
+
catch (_) {
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function childNameFor(t, elements) {
|
|
471
|
+
const im = t.source._isolateModule;
|
|
472
|
+
const depth = scopesOf(t.source.namespace).length;
|
|
473
|
+
for (const el of elements) {
|
|
474
|
+
let ns;
|
|
475
|
+
try {
|
|
476
|
+
ns = im.getNamespace(el);
|
|
477
|
+
}
|
|
478
|
+
catch (_) {
|
|
479
|
+
ns = undefined;
|
|
480
|
+
}
|
|
481
|
+
if (!ns)
|
|
482
|
+
continue;
|
|
483
|
+
for (let i = depth; i < ns.length; i++) {
|
|
484
|
+
if (ns[i].type !== 'total')
|
|
485
|
+
continue;
|
|
486
|
+
const owner = owners.get(keyOf(ns.slice(0, i + 1)));
|
|
487
|
+
if (owner)
|
|
488
|
+
return nameOf(owner);
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
function report103(component, t, selector, severity) {
|
|
495
|
+
if (!once(`SYG103:${severity}:${t.name}:${selector}`))
|
|
496
|
+
return;
|
|
497
|
+
reportSafely('SYG103', {
|
|
498
|
+
component,
|
|
499
|
+
severity,
|
|
500
|
+
message: `DOM.select('${selector}') in ${t.name} has not matched any element ${t.name} rendered` +
|
|
501
|
+
(severity === 'warn' ? ` (after ${t.renders} renders)` : ''),
|
|
502
|
+
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`,
|
|
503
|
+
data: { selector, renders: t.renders },
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function sweep(component, t, escalate) {
|
|
507
|
+
if (t.disposed)
|
|
508
|
+
return;
|
|
509
|
+
const root = rootElementOf(t);
|
|
510
|
+
if (!root || typeof root.querySelectorAll !== 'function')
|
|
511
|
+
return;
|
|
512
|
+
const im = t.source._isolateModule;
|
|
513
|
+
const checker = new ScopeChecker(t.source.namespace, im);
|
|
514
|
+
let unmatched = false;
|
|
515
|
+
for (const selector of t.selectors) {
|
|
516
|
+
const matchKey = `${t.name}\u0000${selector}`;
|
|
517
|
+
if (matched.has(matchKey) || t.crossed.has(selector))
|
|
518
|
+
continue;
|
|
519
|
+
let all;
|
|
520
|
+
try {
|
|
521
|
+
all = Array.prototype.slice.call(root.querySelectorAll(selector));
|
|
522
|
+
if (root.matches(selector))
|
|
523
|
+
all.push(root);
|
|
524
|
+
}
|
|
525
|
+
catch (_) {
|
|
526
|
+
continue; // invalid selector: the DOM driver reports that itself
|
|
527
|
+
}
|
|
528
|
+
const own = all.filter(el => { try {
|
|
529
|
+
return checker.isDirectlyInScope(el);
|
|
530
|
+
}
|
|
531
|
+
catch (_) {
|
|
532
|
+
return false;
|
|
533
|
+
} });
|
|
534
|
+
if (own.length) {
|
|
535
|
+
matched.add(matchKey);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (all.length) {
|
|
539
|
+
t.crossed.add(selector);
|
|
540
|
+
if (!once(`SYG104:${t.name}:${selector}`))
|
|
541
|
+
continue;
|
|
542
|
+
const child = childNameFor(t, all) || 'a child component';
|
|
543
|
+
reportSafely('SYG104', {
|
|
544
|
+
component,
|
|
545
|
+
message: `DOM.select('${selector}') in ${t.name} matches elements inside ${child} (isolated), so ${t.name} never receives their events`,
|
|
546
|
+
fix: `Handle the event in ${child} and send it up with PARENT (read it here with CHILD.select(${child})), or use EVENTS`,
|
|
547
|
+
data: { selector, child },
|
|
548
|
+
});
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (appearsInView(component, selector))
|
|
552
|
+
continue;
|
|
553
|
+
unmatched = true;
|
|
554
|
+
if (escalate && t.renders >= timing.minRenders)
|
|
555
|
+
report103(component, t, selector, 'warn');
|
|
556
|
+
else
|
|
557
|
+
report103(component, t, selector, 'info');
|
|
558
|
+
}
|
|
559
|
+
if (unmatched && !escalate) {
|
|
560
|
+
clearTimeout(t.idleTimer);
|
|
561
|
+
t.idleTimer = unref(setTimeout(() => { t.idleTimer = undefined; sweep(component, t, true); }, timing.idleMs));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* inspect (2B, read-only): a component's intent selectors on a real DOM source
|
|
566
|
+
* and whether a sweep has seen them match (true), only match inside a child
|
|
567
|
+
* (crossed), or neither yet. undefined when the component isn't tracked.
|
|
568
|
+
*/
|
|
569
|
+
function selectorStatus(component) {
|
|
570
|
+
const t = tracked.get(component);
|
|
571
|
+
return t && t.selectors.map(selector => ({
|
|
572
|
+
selector,
|
|
573
|
+
matched: matched.has(`${t.name}\u0000${selector}`) ? true : t.crossed.has(selector) ? false : null,
|
|
574
|
+
crossed: t.crossed.has(selector),
|
|
575
|
+
}));
|
|
576
|
+
}
|
|
577
|
+
const domCheck = {
|
|
578
|
+
id: 'dom',
|
|
579
|
+
onSelector(source, selector) {
|
|
580
|
+
const ns = source && source.namespace;
|
|
581
|
+
if (!Array.isArray(ns) || typeof selector !== 'string' || !selector)
|
|
582
|
+
return;
|
|
583
|
+
const prefix = ns.filter(s => s.type === 'selector').map(s => s.scope).join(' ');
|
|
584
|
+
pending.push({ key: keyOf(scopesOf(ns)), selector: prefix ? `${prefix} ${selector}` : selector });
|
|
585
|
+
if (pending.length > 1000)
|
|
586
|
+
pending.shift();
|
|
587
|
+
},
|
|
588
|
+
onIntent(component) {
|
|
589
|
+
const claimed = pending;
|
|
590
|
+
pending = [];
|
|
591
|
+
const source = component && component.sources && component.sources[component.DOMSourceName || 'DOM'];
|
|
592
|
+
if (!source || !source._isolateModule || !Array.isArray(source.namespace))
|
|
593
|
+
return;
|
|
594
|
+
const key = keyOf(scopesOf(source.namespace));
|
|
595
|
+
const name = nameOf(component);
|
|
596
|
+
owners.set(key, component);
|
|
597
|
+
ownerKeys.set(component, key);
|
|
598
|
+
const selectors = [...new Set(claimed.filter(p => p.key === key).map(p => p.selector))];
|
|
599
|
+
if (selectors.length)
|
|
600
|
+
tracked.set(component, { name, source, selectors, renders: 0, crossed: new Set() });
|
|
601
|
+
},
|
|
602
|
+
onRender(component) {
|
|
603
|
+
const t = tracked.get(component);
|
|
604
|
+
if (!t || t.disposed)
|
|
605
|
+
return;
|
|
606
|
+
t.renders++;
|
|
607
|
+
clearTimeout(t.idleTimer);
|
|
608
|
+
t.idleTimer = undefined;
|
|
609
|
+
if (t.settleTimer)
|
|
610
|
+
return;
|
|
611
|
+
t.settleTimer = unref(setTimeout(() => {
|
|
612
|
+
t.settleTimer = undefined;
|
|
613
|
+
sweep(component, t, false);
|
|
614
|
+
}, timing.settleMs));
|
|
615
|
+
},
|
|
616
|
+
onDispose(component) {
|
|
617
|
+
const key = ownerKeys.get(component);
|
|
618
|
+
if (key !== undefined && owners.get(key) === component)
|
|
619
|
+
owners.delete(key);
|
|
620
|
+
const t = tracked.get(component);
|
|
621
|
+
if (!t)
|
|
622
|
+
return;
|
|
623
|
+
t.disposed = true;
|
|
624
|
+
clearTimeout(t.settleTimer);
|
|
625
|
+
clearTimeout(t.idleTimer);
|
|
626
|
+
tracked.delete(component);
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const DOCS_BASE_URL = 'https://sygnal.js.org';
|
|
631
|
+
const CODE_SEVERITY = {
|
|
632
|
+
SYG101: 'warn',
|
|
633
|
+
SYG102: 'info',
|
|
634
|
+
SYG103: 'info',
|
|
635
|
+
SYG104: 'warn',
|
|
636
|
+
SYG105: 'info',
|
|
637
|
+
SYG106: 'warn',
|
|
638
|
+
SYG110: 'warn',
|
|
639
|
+
SYG111: 'warn',
|
|
640
|
+
SYG201: 'warn',
|
|
641
|
+
SYG202: 'warn',
|
|
642
|
+
SYG301: 'error',
|
|
643
|
+
SYG401: 'warn',
|
|
644
|
+
SYG900: 'warn',
|
|
645
|
+
// ---- 1E retrofit codes (SYG206-299, 402-499, 601-699, 901-999) ----
|
|
646
|
+
SYG206: 'error',
|
|
647
|
+
SYG207: 'warn',
|
|
648
|
+
SYG208: 'warn',
|
|
649
|
+
SYG209: 'error',
|
|
650
|
+
SYG210: 'warn',
|
|
651
|
+
SYG211: 'error',
|
|
652
|
+
SYG212: 'error',
|
|
653
|
+
SYG213: 'warn',
|
|
654
|
+
SYG214: 'error',
|
|
655
|
+
SYG215: 'error',
|
|
656
|
+
SYG216: 'error',
|
|
657
|
+
SYG217: 'warn',
|
|
658
|
+
SYG218: 'error',
|
|
659
|
+
SYG219: 'warn',
|
|
660
|
+
SYG220: 'warn',
|
|
661
|
+
SYG402: 'error',
|
|
662
|
+
SYG403: 'error',
|
|
663
|
+
SYG404: 'error',
|
|
664
|
+
SYG405: 'warn',
|
|
665
|
+
SYG406: 'error',
|
|
666
|
+
SYG407: 'error',
|
|
667
|
+
SYG408: 'error',
|
|
668
|
+
SYG409: 'warn',
|
|
669
|
+
SYG410: 'error',
|
|
670
|
+
SYG411: 'error',
|
|
671
|
+
SYG412: 'error',
|
|
672
|
+
SYG413: 'error',
|
|
673
|
+
SYG414: 'error',
|
|
674
|
+
SYG415: 'error',
|
|
675
|
+
SYG416: 'error',
|
|
676
|
+
SYG417: 'warn',
|
|
677
|
+
SYG418: 'error',
|
|
678
|
+
SYG419: 'error',
|
|
679
|
+
SYG420: 'error',
|
|
680
|
+
SYG601: 'error',
|
|
681
|
+
SYG602: 'error',
|
|
682
|
+
SYG603: 'error',
|
|
683
|
+
SYG604: 'error',
|
|
684
|
+
SYG605: 'error',
|
|
685
|
+
SYG606: 'error',
|
|
686
|
+
SYG607: 'error',
|
|
687
|
+
SYG901: 'error',
|
|
688
|
+
SYG902: 'error',
|
|
689
|
+
SYG903: 'error',
|
|
690
|
+
// ---- end 1E retrofit codes ----
|
|
691
|
+
};
|
|
692
|
+
const CODE_TITLES = {
|
|
693
|
+
// SYG1xx wiring (1A)
|
|
694
|
+
SYG101: 'Intent action has no model entry',
|
|
695
|
+
SYG102: 'Model entry is unreachable',
|
|
696
|
+
SYG103: 'Intent selector matched no rendered element',
|
|
697
|
+
SYG104: 'Intent selector crosses an isolation boundary',
|
|
698
|
+
SYG105: 'EVENTS type selected but never emitted, or emitted but never selected',
|
|
699
|
+
SYG106: 'Parent prop is overwritten by a reserved view argument',
|
|
700
|
+
SYG110: 'Intent selector not present in the component view',
|
|
701
|
+
SYG111: 'Controlled input has no input listener',
|
|
702
|
+
// SYG2xx state & reducers (1A)
|
|
703
|
+
SYG201: 'STATE reducer dropped keys from the previous state',
|
|
704
|
+
SYG202: 'STATE reducer returned undefined',
|
|
705
|
+
// SYG3xx streams & intent construction (1A)
|
|
706
|
+
SYG301: 'RxJS operator used on an xstream stream',
|
|
707
|
+
// SYG4xx collections / switchable / sub-components / context (1A)
|
|
708
|
+
SYG401: "Collection 'from' field is missing or not an array",
|
|
709
|
+
// SYG5xx strict mode / canonical forms (2A; dev-plans/PLAN-1-canonical-forms.md)
|
|
710
|
+
SYG501: 'View uses positional arguments',
|
|
711
|
+
SYG502: 'STATE reducer signals no change without ABORT',
|
|
712
|
+
SYG503: 'Side effect in a STATE reducer that returns ABORT',
|
|
713
|
+
SYG504: "'ACTION | SINK' shorthand model key",
|
|
714
|
+
SYG505: 'Non-canonical EVENTS emit',
|
|
715
|
+
SYG506: 'CHILD.select() with a string component name',
|
|
716
|
+
SYG507: 'Prop drilled through more than 2 component levels',
|
|
717
|
+
// SYG9xx internal (0B)
|
|
718
|
+
SYG900: 'A diagnostics check threw',
|
|
719
|
+
// ---- 1E retrofit codes (SYG206-299, 402-499, 601-699, 901-999) ----
|
|
720
|
+
SYG206: 'Invalid calculated field definition',
|
|
721
|
+
SYG207: 'Calculated field shadows an initialState key',
|
|
722
|
+
SYG208: 'Calculated field depends on an unknown key',
|
|
723
|
+
SYG209: 'Circular calculated field dependency',
|
|
724
|
+
SYG210: 'INITIALIZE used with a sink other than STATE',
|
|
725
|
+
SYG211: 'Invalid model shorthand entry',
|
|
726
|
+
SYG212: 'Model entry is not a function or an object of sinks',
|
|
727
|
+
SYG213: 'Duplicate model entry for the same action and sink',
|
|
728
|
+
SYG214: 'EFFECT handler threw',
|
|
729
|
+
SYG215: 'next() delay is not a number',
|
|
730
|
+
SYG216: 'Reducer threw',
|
|
731
|
+
SYG217: 'Driver-sink reducer returned undefined',
|
|
732
|
+
SYG218: 'Reducer returned an unsupported type',
|
|
733
|
+
SYG219: 'EFFECT handler returned a value',
|
|
734
|
+
SYG220: 'Calculated field threw',
|
|
735
|
+
SYG402: 'Context is not an object',
|
|
736
|
+
SYG403: 'Invalid context entry',
|
|
737
|
+
SYG404: 'Context stream errored',
|
|
738
|
+
SYG405: 'Sub-component has initialState without isolatedState',
|
|
739
|
+
SYG406: 'View threw',
|
|
740
|
+
SYG407: 'onError handler threw',
|
|
741
|
+
SYG408: 'Sub-component failed to instantiate',
|
|
742
|
+
SYG409: 'Child tried to update a calculated field',
|
|
743
|
+
SYG410: "Invalid sub-component 'state' prop",
|
|
744
|
+
SYG411: "Collection 'of' is missing or not a component",
|
|
745
|
+
SYG412: "Invalid Collection 'from' prop",
|
|
746
|
+
SYG413: 'Unnamed component factory not found',
|
|
747
|
+
SYG414: 'Component not found',
|
|
748
|
+
SYG415: "Switchable 'of' is missing or invalid",
|
|
749
|
+
SYG416: "Switchable 'current' is missing or invalid",
|
|
750
|
+
SYG417: 'Portal target not found',
|
|
751
|
+
SYG418: 'Invalid Collection sort prop',
|
|
752
|
+
SYG419: "Invalid switchable() 'name$' parameter",
|
|
753
|
+
SYG420: 'JSX tag is undefined',
|
|
754
|
+
SYG601: 'Missing or invalid sources',
|
|
755
|
+
SYG602: 'Intent is not a function',
|
|
756
|
+
SYG603: 'Intent returned an invalid value',
|
|
757
|
+
SYG604: 'Invalid hmrActions',
|
|
758
|
+
SYG605: "Intent action name contains '|'",
|
|
759
|
+
SYG606: 'calculated is not an object',
|
|
760
|
+
SYG607: 'State source not found',
|
|
761
|
+
SYG901: 'Sub-component sink stream errored',
|
|
762
|
+
SYG902: 'EFFECT stream errored',
|
|
763
|
+
SYG903: 'Component factory returned invalid sinks',
|
|
764
|
+
// ---- end 1E retrofit codes ----
|
|
765
|
+
};
|
|
766
|
+
// 2A: strict-mode (SYG5xx) default severities. Kept OUT of CODE_SEVERITY so the
|
|
767
|
+
// main bundle doesn't carry them (D29: zero bytes); the 'sygnal/diagnostics'
|
|
768
|
+
// entry registers them (registerCodes) and its reports pass the severity
|
|
769
|
+
// explicitly. sygnal-check keeps the same table in sygnal-check/src/codes.js.
|
|
770
|
+
const STRICT_CODE_SEVERITY = {
|
|
771
|
+
SYG501: 'warn',
|
|
772
|
+
SYG502: 'warn',
|
|
773
|
+
SYG503: 'warn',
|
|
774
|
+
SYG504: 'warn',
|
|
775
|
+
SYG505: 'warn',
|
|
776
|
+
SYG506: 'warn',
|
|
777
|
+
SYG507: 'info',
|
|
778
|
+
};
|
|
779
|
+
function getCodeInfo(code) {
|
|
780
|
+
const severity = CODE_SEVERITY[code];
|
|
781
|
+
return severity && { code, severity, title: CODE_TITLES[code] || '', docsSlug: code.toLowerCase() };
|
|
782
|
+
}
|
|
783
|
+
function listCodes() {
|
|
784
|
+
return Object.keys(CODE_SEVERITY).map(c => getCodeInfo(c));
|
|
785
|
+
}
|
|
786
|
+
function registerCodes(entries) {
|
|
787
|
+
for (const [code, severity, title] of entries) {
|
|
788
|
+
CODE_SEVERITY[code] = severity;
|
|
789
|
+
CODE_TITLES[code] = title;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function docsUrlFor(code) {
|
|
793
|
+
return `${DOCS_BASE_URL}/reference/errors#${code.toLowerCase()}`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
registerCodes(Object.keys(STRICT_CODE_SEVERITY).map(code => [code, STRICT_CODE_SEVERITY[code], CODE_TITLES[code]]));
|
|
797
|
+
/**
|
|
798
|
+
* Turn runtime strict checks on (true), off (false), or back to the default
|
|
799
|
+
* (undefined → globalThis.__SYGNAL_STRICT__ === true).
|
|
800
|
+
*/
|
|
801
|
+
function configureStrict(on) {
|
|
802
|
+
const core = bridge();
|
|
803
|
+
if (core)
|
|
804
|
+
core.strict = on;
|
|
805
|
+
}
|
|
806
|
+
/** Whether runtime strict checks are on. */
|
|
807
|
+
function isStrictEnabled() {
|
|
808
|
+
const flag = bridge()?.strict;
|
|
809
|
+
return flag === undefined ? globalThis.__SYGNAL_STRICT__ === true : flag === true;
|
|
810
|
+
}
|
|
811
|
+
const strictReport = (code, details) => report(code, { severity: STRICT_CODE_SEVERITY[code], ...details });
|
|
812
|
+
const strictCheck = {
|
|
813
|
+
id: 'strict',
|
|
814
|
+
onModel(component) {
|
|
815
|
+
if (!isStrictEnabled() || !component)
|
|
816
|
+
return;
|
|
817
|
+
const name = nameOf(component);
|
|
818
|
+
// SYG501 — view(props, state, context, peers): positional use of the 2nd+ args.
|
|
819
|
+
// (A default value or a rest parameter stops Function.length, so this can
|
|
820
|
+
// miss `(props, state = {})`, but it never flags a single-argument view.)
|
|
821
|
+
const view = component.view;
|
|
822
|
+
if (typeof view === 'function' && view.length > 1 && !view.__sygnalLazy && once(`SYG501:${name}`)) {
|
|
823
|
+
const extra = ['state', 'context', 'peers'].slice(0, view.length - 1);
|
|
824
|
+
strictReport('SYG501', {
|
|
825
|
+
component,
|
|
826
|
+
message: `The view takes ${view.length} positional arguments (props, ${extra.join(', ')})`,
|
|
827
|
+
fix: `Destructure the first argument instead: function ${name}({ ${extra.join(', ')}, ...props })`,
|
|
828
|
+
data: { arity: view.length },
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
// SYG504 — 'ACTION | SINK' shorthand keys
|
|
832
|
+
const model = component.model;
|
|
833
|
+
if (!model || typeof model !== 'object')
|
|
834
|
+
return;
|
|
835
|
+
for (const key of Object.keys(model)) {
|
|
836
|
+
if (!key.includes('|'))
|
|
837
|
+
continue;
|
|
838
|
+
const [action, sink] = key.split('|').map(s => s.trim());
|
|
839
|
+
if (!action || !sink || !once(`SYG504:${name}:${key}`))
|
|
840
|
+
continue;
|
|
841
|
+
const rewrite = sink === 'EVENTS'
|
|
842
|
+
? `${action}: { EVENTS: event('TYPE', (state, data) => payload) }`
|
|
843
|
+
: `${action}: { ${sink}: (state, data, next) => ... }`;
|
|
844
|
+
strictReport('SYG504', {
|
|
845
|
+
component,
|
|
846
|
+
message: `Model key '${key}' uses the 'ACTION | SINK' shorthand`,
|
|
847
|
+
fix: `Use the object form: ${rewrite} (merge it into an existing '${action}' entry if there is one)`,
|
|
848
|
+
data: { key, action, sink },
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
onReducer(component, action, prevState, nextState) {
|
|
853
|
+
if (!isStrictEnabled() || isInternalAction(action))
|
|
854
|
+
return; // incl. INITIALIZE/HYDRATE/DISPOSE
|
|
855
|
+
// undefined is SYG202 (state check, every mode); only the identical object here.
|
|
856
|
+
if (!prevState || typeof prevState !== 'object' || nextState === undefined)
|
|
857
|
+
return;
|
|
858
|
+
let same = nextState === prevState;
|
|
859
|
+
if (!same && typeof component?.addCalculated === 'function') {
|
|
860
|
+
// with calculated fields the reducer saw addCalculated(prev) (memoized: same object)
|
|
861
|
+
try {
|
|
862
|
+
same = component.addCalculated(prevState) === nextState;
|
|
863
|
+
}
|
|
864
|
+
catch (_) { }
|
|
865
|
+
}
|
|
866
|
+
const name = nameOf(component);
|
|
867
|
+
if (!same || !once(`SYG502:${name}:${action}`))
|
|
868
|
+
return;
|
|
869
|
+
strictReport('SYG502', {
|
|
870
|
+
component,
|
|
871
|
+
message: `The STATE reducer for '${action}' returned the unchanged state object`,
|
|
872
|
+
fix: `Return ABORT for "no change": ${action}: (state, data) => cond ? { ...state, ... } : ABORT`,
|
|
873
|
+
data: { action },
|
|
874
|
+
});
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* SYG301 — an RxJS operator was called on an xstream stream (error).
|
|
880
|
+
*
|
|
881
|
+
* Without help this fails with a bare "x.switchMap is not a function" inside
|
|
882
|
+
* the intent function. Mechanism: when 'sygnal/diagnostics' is loaded, every
|
|
883
|
+
* RxJS operator name below that xstream's Stream does NOT already have is
|
|
884
|
+
* defined on Stream.prototype (non-enumerable) as a method that throws an
|
|
885
|
+
* enriched TypeError: same "stream.<op> is not a function" shape, plus the
|
|
886
|
+
* xstream equivalent and the SYG301 docs link. The error is also reported
|
|
887
|
+
* (collected / printed per the diagnostics mode; in 'error' mode report()
|
|
888
|
+
* throws the DiagnosticError itself). It works anywhere a stream is used
|
|
889
|
+
* (intent, peers, drivers), with no hook in component.ts.
|
|
890
|
+
*
|
|
891
|
+
* Caveat: `typeof stream.pipe === 'function'` is true while this entry is
|
|
892
|
+
* loaded (dev only).
|
|
893
|
+
*/
|
|
894
|
+
/** RxJS operator -> xstream equivalent. */
|
|
895
|
+
const RXJS_HINTS = {
|
|
896
|
+
pipe: `chain xstream operators directly (.map(...).filter(...)), or use .compose(operator) for extra operators`,
|
|
897
|
+
switchMap: `.map(x => inner$).flatten()`,
|
|
898
|
+
mergeMap: `.map(x => inner$).compose(flattenConcurrently) (import { flattenConcurrently } from 'sygnal')`,
|
|
899
|
+
flatMap: `.map(x => inner$).compose(flattenConcurrently) (import { flattenConcurrently } from 'sygnal')`,
|
|
900
|
+
concatMap: `.map(x => inner$).compose(flattenSequentially) (import { flattenSequentially } from 'sygnal')`,
|
|
901
|
+
exhaustMap: `.map(x => inner$).flatten() (xstream has no exhaustMap; ignore new values while busy with .filter())`,
|
|
902
|
+
switchAll: `.flatten()`,
|
|
903
|
+
mergeAll: `.compose(flattenConcurrently) (import { flattenConcurrently } from 'sygnal')`,
|
|
904
|
+
concatAll: `.compose(flattenSequentially) (import { flattenSequentially } from 'sygnal')`,
|
|
905
|
+
debounceTime: `.compose(debounce(ms)) (import { debounce } from 'sygnal')`,
|
|
906
|
+
debounce: `.compose(debounce(ms)) (import { debounce } from 'sygnal')`,
|
|
907
|
+
throttleTime: `.compose(throttle(ms)) (import { throttle } from 'sygnal')`,
|
|
908
|
+
throttle: `.compose(throttle(ms)) (import { throttle } from 'sygnal')`,
|
|
909
|
+
auditTime: `.compose(throttle(ms)) (import { throttle } from 'sygnal')`,
|
|
910
|
+
delay: `.compose(delay(ms)) (import { delay } from 'sygnal')`,
|
|
911
|
+
distinctUntilChanged: `.compose(dropRepeats()) (import { dropRepeats } from 'sygnal')`,
|
|
912
|
+
withLatestFrom: `.compose(sampleCombine(other$)) (import { sampleCombine } from 'sygnal')`,
|
|
913
|
+
combineLatest: `xs.combine(a$, b$) (import { xs } from 'sygnal')`,
|
|
914
|
+
combineLatestWith: `xs.combine(a$, b$) (import { xs } from 'sygnal')`,
|
|
915
|
+
merge: `xs.merge(a$, b$) (import { xs } from 'sygnal')`,
|
|
916
|
+
mergeWith: `xs.merge(a$, b$) (import { xs } from 'sygnal')`,
|
|
917
|
+
concat: `concat(a$, b$) (import { concat } from 'sygnal')`,
|
|
918
|
+
scan: `.fold((acc, x) => next, seed)`,
|
|
919
|
+
reduce: `.fold((acc, x) => next, seed).last()`,
|
|
920
|
+
skip: `.drop(n)`,
|
|
921
|
+
first: `.take(1)`,
|
|
922
|
+
takeUntil: `.endWhen(other$)`,
|
|
923
|
+
tap: `.debug(fn)`,
|
|
924
|
+
do: `.debug(fn)`,
|
|
925
|
+
catchError: `.replaceError(err => fallback$)`,
|
|
926
|
+
catch: `.replaceError(err => fallback$)`,
|
|
927
|
+
pluck: `.map(x => x.field)`,
|
|
928
|
+
share: `xstream streams are already shared (hot); use .remember() to replay the latest value`,
|
|
929
|
+
shareReplay: `.remember()`,
|
|
930
|
+
publishReplay: `.remember()`,
|
|
931
|
+
finalize: `.debug({ complete: fn }) or handle completion in the subscriber`,
|
|
932
|
+
};
|
|
933
|
+
const SYG301_URL = docsUrlFor('SYG301');
|
|
934
|
+
function rxjsError(op) {
|
|
935
|
+
const hint = RXJS_HINTS[op];
|
|
936
|
+
const message = `stream.${op} is not a function: '${op}' is an RxJS operator, but Sygnal streams are xstream streams`;
|
|
937
|
+
const fix = `Use ${hint}`;
|
|
938
|
+
// In 'error' mode report() throws the DiagnosticError itself (we are about to throw anyway).
|
|
939
|
+
const d = report('SYG301', { message, fix, data: { operator: op, hint } });
|
|
940
|
+
const err = new TypeError(d ? d.text : `[Sygnal SYG301] ${message}. ${fix}. ${SYG301_URL}`);
|
|
941
|
+
err.diagnostic = d;
|
|
942
|
+
return err;
|
|
943
|
+
}
|
|
944
|
+
/** Install the hints on xstream's Stream.prototype. Returns an uninstall function. */
|
|
945
|
+
function installRxjsHints(proto) {
|
|
946
|
+
const added = [];
|
|
947
|
+
if (!proto)
|
|
948
|
+
return () => { };
|
|
949
|
+
for (const op of Object.keys(RXJS_HINTS)) {
|
|
950
|
+
if (op in proto)
|
|
951
|
+
continue;
|
|
952
|
+
Object.defineProperty(proto, op, {
|
|
953
|
+
configurable: true,
|
|
954
|
+
writable: true,
|
|
955
|
+
enumerable: false,
|
|
956
|
+
value: function rxjsOperatorHint() { throw rxjsError(op); },
|
|
957
|
+
});
|
|
958
|
+
added.push(op);
|
|
959
|
+
}
|
|
960
|
+
return () => { for (const op of added)
|
|
961
|
+
delete proto[op]; };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
let records = new Map();
|
|
965
|
+
let pendingSelects = [];
|
|
966
|
+
/** every EVENTS type seen on the bus */
|
|
967
|
+
let busTypes = new Set();
|
|
968
|
+
onReset(() => {
|
|
969
|
+
records = new Map();
|
|
970
|
+
pendingSelects = [];
|
|
971
|
+
busTypes = new Set();
|
|
972
|
+
});
|
|
973
|
+
const uniq = (list) => [...new Set(list)];
|
|
974
|
+
function kindOf(c) {
|
|
975
|
+
const s = (c && c.sources) || {};
|
|
976
|
+
if (typeof s.__parentComponentNumber !== 'number')
|
|
977
|
+
return 'root';
|
|
978
|
+
// Collection items get PARENT: null (src/component.ts instantiateCollection)
|
|
979
|
+
if ('PARENT' in s && s.PARENT === null)
|
|
980
|
+
return 'collection-item';
|
|
981
|
+
// switchable() adds a lower-case `state` source next to the component's own (src/switchable.ts)
|
|
982
|
+
if (c.stateSourceName !== 'state' && 'state' in s && s.state !== s[c.stateSourceName])
|
|
983
|
+
return 'switchable';
|
|
984
|
+
return 'child';
|
|
985
|
+
}
|
|
986
|
+
function ensure(c) {
|
|
987
|
+
let r = records.get(c);
|
|
988
|
+
if (!r) {
|
|
989
|
+
const parent = c && c.sources && c.sources.__parentComponentNumber;
|
|
990
|
+
r = {
|
|
991
|
+
instance: c,
|
|
992
|
+
id: String(c && c._componentNumber),
|
|
993
|
+
name: nameOf(c),
|
|
994
|
+
parentId: typeof parent === 'number' ? String(parent) : null,
|
|
995
|
+
kind: kindOf(c),
|
|
996
|
+
intentActions: [],
|
|
997
|
+
injected: new Set(),
|
|
998
|
+
modelMap: {},
|
|
999
|
+
fired: new Set(),
|
|
1000
|
+
eventsSelected: new Set(),
|
|
1001
|
+
eventsEmitted: new Set(),
|
|
1002
|
+
};
|
|
1003
|
+
records.set(c, r);
|
|
1004
|
+
}
|
|
1005
|
+
return r;
|
|
1006
|
+
}
|
|
1007
|
+
let devtools;
|
|
1008
|
+
function attachDevtools() {
|
|
1009
|
+
const dt = globalThis.__SYGNAL_DEVTOOLS__;
|
|
1010
|
+
if (!dt || typeof dt !== 'object')
|
|
1011
|
+
return;
|
|
1012
|
+
devtools = dt;
|
|
1013
|
+
if (dt.inspect !== inspect) {
|
|
1014
|
+
try {
|
|
1015
|
+
dt.inspect = inspect;
|
|
1016
|
+
}
|
|
1017
|
+
catch (_) { /* frozen: ignore */ }
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const inspectCheck = {
|
|
1021
|
+
id: 'inspect',
|
|
1022
|
+
onBusSelect(type) {
|
|
1023
|
+
if (type === undefined || type === null || type === '')
|
|
1024
|
+
pendingSelects.push('*');
|
|
1025
|
+
else
|
|
1026
|
+
for (const t of [].concat(type))
|
|
1027
|
+
if (typeof t === 'string')
|
|
1028
|
+
pendingSelects.push(t);
|
|
1029
|
+
if (pendingSelects.length > 1000)
|
|
1030
|
+
pendingSelects.shift();
|
|
1031
|
+
},
|
|
1032
|
+
onIntent(component, actionNames) {
|
|
1033
|
+
attachDevtools();
|
|
1034
|
+
const r = ensure(component);
|
|
1035
|
+
const intent$ = component && component.intent$;
|
|
1036
|
+
r.intentActions = intent$ && typeof intent$.addListener === 'function' ? null : (actionNames || []);
|
|
1037
|
+
r.injected = new Set((intent$ && typeof intent$ === 'object' && intent$.__sygnalTestActions) || []);
|
|
1038
|
+
for (const t of pendingSelects)
|
|
1039
|
+
r.eventsSelected.add(t);
|
|
1040
|
+
pendingSelects = [];
|
|
1041
|
+
},
|
|
1042
|
+
onModel(component, modelMap) {
|
|
1043
|
+
const r = ensure(component);
|
|
1044
|
+
r.modelMap = modelMap || {};
|
|
1045
|
+
const model$ = component && component.model$;
|
|
1046
|
+
const events$ = model$ && model$.EVENTS;
|
|
1047
|
+
if (events$ && typeof events$.map === 'function' && !events$.__sygnalInspect) {
|
|
1048
|
+
const tapped = events$.map((ev) => {
|
|
1049
|
+
if (ev && typeof ev.type === 'string')
|
|
1050
|
+
r.eventsEmitted.add(ev.type);
|
|
1051
|
+
return ev;
|
|
1052
|
+
});
|
|
1053
|
+
tapped.__sygnalInspect = true;
|
|
1054
|
+
model$.EVENTS = tapped;
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
onReducer(component, action) {
|
|
1058
|
+
const r = records.get(component);
|
|
1059
|
+
if (r && typeof action === 'string')
|
|
1060
|
+
r.fired.add(action);
|
|
1061
|
+
},
|
|
1062
|
+
onBusEmit(type) {
|
|
1063
|
+
if (typeof type === 'string')
|
|
1064
|
+
busTypes.add(type);
|
|
1065
|
+
},
|
|
1066
|
+
onDispose(component) {
|
|
1067
|
+
records.delete(component);
|
|
1068
|
+
},
|
|
1069
|
+
};
|
|
1070
|
+
const slim = (d) => {
|
|
1071
|
+
const out = { code: d.code, severity: d.severity, message: d.message };
|
|
1072
|
+
if (d.component)
|
|
1073
|
+
out.component = d.component;
|
|
1074
|
+
if (d.fix)
|
|
1075
|
+
out.fix = d.fix;
|
|
1076
|
+
if (d.docsUrl)
|
|
1077
|
+
out.docsUrl = d.docsUrl;
|
|
1078
|
+
if (d.data !== undefined) {
|
|
1079
|
+
// keep it JSON-safe (data can hold errors, vnodes, ...)
|
|
1080
|
+
try {
|
|
1081
|
+
out.data = JSON.parse(JSON.stringify(d.data));
|
|
1082
|
+
}
|
|
1083
|
+
catch (_) { /* drop */ }
|
|
1084
|
+
}
|
|
1085
|
+
return out;
|
|
1086
|
+
};
|
|
1087
|
+
function actionsOf(r) {
|
|
1088
|
+
const intent = (r.intentActions || []).filter(a => !r.injected.has(a));
|
|
1089
|
+
const names = uniq([...intent, ...Object.keys(r.modelMap)]).filter(a => typeof a === 'string' && !a.startsWith('__'));
|
|
1090
|
+
return names.map(name => ({
|
|
1091
|
+
name,
|
|
1092
|
+
trigger: BUILTIN_ACTIONS.has(name) ? 'builtin'
|
|
1093
|
+
: intent.includes(name) ? 'intent'
|
|
1094
|
+
: !r.injected.has(name) && r.fired.has(name) ? 'next'
|
|
1095
|
+
: 'unknown',
|
|
1096
|
+
sinks: [...(r.modelMap[name] || [])],
|
|
1097
|
+
}));
|
|
1098
|
+
}
|
|
1099
|
+
function stateKeysOf(c, calculated) {
|
|
1100
|
+
const s = c && c.currentState;
|
|
1101
|
+
return isPlainObject(s) ? Object.keys(s).filter(k => !calculated.includes(k)) : [];
|
|
1102
|
+
}
|
|
1103
|
+
const isScope = (s) => s.startsWith('.___');
|
|
1104
|
+
function mockSelectors(mock) {
|
|
1105
|
+
const out = {};
|
|
1106
|
+
mock.listeners.forEach((path, key) => {
|
|
1107
|
+
const scope = path.filter(isScope).pop();
|
|
1108
|
+
const id = mock.scopeIds.get(scope || '');
|
|
1109
|
+
if (id === undefined)
|
|
1110
|
+
return;
|
|
1111
|
+
const sels = path.filter(s => !isScope(s)).join(' ').split(/[\s>]+/).filter(Boolean);
|
|
1112
|
+
const page = !sels.length || /^(document|body)$/.test(sels[0]);
|
|
1113
|
+
const { own, child } = mock.vtree && !page ? mock.probe(sels, scope) : { own: false, child: undefined };
|
|
1114
|
+
(out[id] = out[id] || []).push({
|
|
1115
|
+
selector: sels.join(' '),
|
|
1116
|
+
events: uniq(mock.evTypes[key] || []),
|
|
1117
|
+
matched: mock.vtree && !page ? own : null,
|
|
1118
|
+
isolationHit: !own && child ? mock.owners.get(child) || 'a child component' : null,
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
return out;
|
|
1122
|
+
}
|
|
1123
|
+
function selectorsOf(r, options, diags) {
|
|
1124
|
+
const given = options.selectors && options.selectors[r.id];
|
|
1125
|
+
if (given)
|
|
1126
|
+
return given.map(s => ({ ...s }));
|
|
1127
|
+
return (selectorStatus(r.instance) || []).map(({ selector, matched, crossed }) => {
|
|
1128
|
+
const hit = diags.find(d => d.code === 'SYG104' && d.component === r.name && d.data && d.data.selector === selector);
|
|
1129
|
+
const missed = diags.some(d => d.code === 'SYG103' && d.component === r.name && d.data && d.data.selector === selector);
|
|
1130
|
+
return {
|
|
1131
|
+
selector,
|
|
1132
|
+
events: null,
|
|
1133
|
+
matched: matched === null && missed ? false : matched,
|
|
1134
|
+
isolationHit: crossed ? ((hit && hit.data.child) || 'a child component') : null,
|
|
1135
|
+
};
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
function childrenOf(r, all) {
|
|
1139
|
+
const via = { root: 'tag', child: 'tag', 'collection-item': 'collection', switchable: 'switchable' };
|
|
1140
|
+
const out = new Map();
|
|
1141
|
+
for (const c of all) {
|
|
1142
|
+
if (c.parentId !== r.id)
|
|
1143
|
+
continue;
|
|
1144
|
+
const key = `${c.name}\u0000${via[c.kind]}`;
|
|
1145
|
+
const entry = out.get(key);
|
|
1146
|
+
if (entry)
|
|
1147
|
+
entry.count = (entry.count || 1) + 1;
|
|
1148
|
+
else
|
|
1149
|
+
out.set(key, { name: c.name, via: via[c.kind], count: 1 });
|
|
1150
|
+
}
|
|
1151
|
+
return [...out.values()];
|
|
1152
|
+
}
|
|
1153
|
+
/** The app graph of the live component instances (see ./public.d.ts). */
|
|
1154
|
+
function inspect(options = {}) {
|
|
1155
|
+
// report SYG105 (info) for EVENTS types selected but never emitted so far
|
|
1156
|
+
try {
|
|
1157
|
+
checkEventBus();
|
|
1158
|
+
}
|
|
1159
|
+
catch (_) { /* 'error' mode: info never throws; be safe */ }
|
|
1160
|
+
let diags = options.diagnostics || [];
|
|
1161
|
+
if (!options.diagnostics) {
|
|
1162
|
+
attachDevtools();
|
|
1163
|
+
try {
|
|
1164
|
+
diags = (devtools && typeof devtools.getDiagnostics === 'function' && devtools.getDiagnostics()) || [];
|
|
1165
|
+
}
|
|
1166
|
+
catch (_) {
|
|
1167
|
+
diags = [];
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
const mock = options.mock;
|
|
1171
|
+
if (mock)
|
|
1172
|
+
options = { ...options, selectors: { ...mockSelectors(mock), ...options.selectors } };
|
|
1173
|
+
const all = [...records.values()];
|
|
1174
|
+
const ids = options.ids && new Set(options.ids.map(String));
|
|
1175
|
+
const recs = ids ? all.filter(r => ids.has(r.id)) : all;
|
|
1176
|
+
const names = new Set(recs.map(r => r.name));
|
|
1177
|
+
const components = recs.map(r => {
|
|
1178
|
+
const c = r.instance;
|
|
1179
|
+
const calculated = c && c.calculated && typeof c.calculated === 'object' ? Object.keys(c.calculated) : [];
|
|
1180
|
+
return {
|
|
1181
|
+
name: r.name,
|
|
1182
|
+
id: r.id,
|
|
1183
|
+
parentId: r.parentId,
|
|
1184
|
+
kind: r.kind,
|
|
1185
|
+
actions: actionsOf(r),
|
|
1186
|
+
stateKeys: stateKeysOf(c, calculated),
|
|
1187
|
+
calculated,
|
|
1188
|
+
contextProvides: c && isPlainObject(c.context) ? Object.keys(c.context) : [],
|
|
1189
|
+
contextConsumes: null,
|
|
1190
|
+
eventsEmitted: [...r.eventsEmitted],
|
|
1191
|
+
eventsSelected: [...r.eventsSelected],
|
|
1192
|
+
children: childrenOf(r, all),
|
|
1193
|
+
selectors: selectorsOf(r, options, diags),
|
|
1194
|
+
diagnostics: diags.filter(d => d && d.component === r.name).map(slim),
|
|
1195
|
+
};
|
|
1196
|
+
});
|
|
1197
|
+
const events = {};
|
|
1198
|
+
const ev = (type) => events[type] || (events[type] = { emitters: [], selectors: [] });
|
|
1199
|
+
const add = (list, name) => { if (!list.includes(name))
|
|
1200
|
+
list.push(name); };
|
|
1201
|
+
for (const c of components) {
|
|
1202
|
+
for (const t of c.eventsEmitted)
|
|
1203
|
+
add(ev(t).emitters, c.name);
|
|
1204
|
+
for (const t of c.eventsSelected)
|
|
1205
|
+
add(ev(t).selectors, c.name);
|
|
1206
|
+
}
|
|
1207
|
+
// emitted on the bus by something that isn't a listed component (e.g. a driver, a pruned component)
|
|
1208
|
+
if (!ids)
|
|
1209
|
+
for (const t of busTypes)
|
|
1210
|
+
ev(t);
|
|
1211
|
+
return {
|
|
1212
|
+
version: 1,
|
|
1213
|
+
source: 'runtime',
|
|
1214
|
+
components,
|
|
1215
|
+
events,
|
|
1216
|
+
diagnostics: diags.filter(d => d && (!d.component || !names.has(d.component))).map(slim),
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
/** Publish inspect() on the core bridge and the devtools object. Returns an uninstall function. */
|
|
1220
|
+
function installInspect() {
|
|
1221
|
+
const core = bridge();
|
|
1222
|
+
if (core)
|
|
1223
|
+
core.inspect = inspect;
|
|
1224
|
+
attachDevtools();
|
|
1225
|
+
return () => {
|
|
1226
|
+
if (core && core.inspect === inspect)
|
|
1227
|
+
core.inspect = undefined;
|
|
1228
|
+
const dt = globalThis.__SYGNAL_DEVTOOLS__;
|
|
1229
|
+
if (dt && dt.inspect === inspect) {
|
|
1230
|
+
try {
|
|
1231
|
+
delete dt.inspect;
|
|
1232
|
+
}
|
|
1233
|
+
catch (_) { /* ignore */ }
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/** Every runtime check in this entry. */
|
|
1239
|
+
const checks = [
|
|
1240
|
+
wiringCheck,
|
|
1241
|
+
stateCheck,
|
|
1242
|
+
eventsCheck,
|
|
1243
|
+
propsCheck,
|
|
1244
|
+
collectionsCheck,
|
|
1245
|
+
domCheck,
|
|
1246
|
+
strictCheck,
|
|
1247
|
+
inspectCheck,
|
|
1248
|
+
];
|
|
1249
|
+
/**
|
|
1250
|
+
* (Re-)register all checks with the core and install the RxJS hints. Called
|
|
1251
|
+
* on import. Idempotent across duplicate copies of this entry: the previous
|
|
1252
|
+
* installation (stored on the core bridge) is removed first.
|
|
1253
|
+
*/
|
|
1254
|
+
function installChecks() {
|
|
1255
|
+
const core = bridge();
|
|
1256
|
+
if (!core)
|
|
1257
|
+
throw new Error(`[Sygnal] 'sygnal/diagnostics' could not find the Sygnal diagnostics core`);
|
|
1258
|
+
if (typeof core.__uninstallChecks === 'function')
|
|
1259
|
+
core.__uninstallChecks();
|
|
1260
|
+
const unregister = checks.map(check => core.registerCheck(check));
|
|
1261
|
+
const uninstallHints = installRxjsHints(Stream && Stream.prototype);
|
|
1262
|
+
const uninstallInspect = installInspect();
|
|
1263
|
+
const uninstall = () => {
|
|
1264
|
+
unregister.forEach(fn => fn());
|
|
1265
|
+
uninstallHints();
|
|
1266
|
+
uninstallInspect();
|
|
1267
|
+
if (core.__uninstallChecks === uninstall)
|
|
1268
|
+
core.__uninstallChecks = undefined;
|
|
1269
|
+
};
|
|
1270
|
+
core.__uninstallChecks = uninstall;
|
|
1271
|
+
return uninstall;
|
|
1272
|
+
}
|
|
1273
|
+
/** Tune the timing of the DOM checks (mostly for tests). */
|
|
1274
|
+
function configureChecks(options = {}) {
|
|
1275
|
+
for (const key of Object.keys(timing)) {
|
|
1276
|
+
if (typeof options[key] === 'number')
|
|
1277
|
+
timing[key] = options[key];
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
/** Forget what the checks have seen and reported (dedupe state, registries). For tests. */
|
|
1281
|
+
function resetChecks() {
|
|
1282
|
+
resetCheckState();
|
|
1283
|
+
}
|
|
1284
|
+
installChecks();
|
|
1285
|
+
|
|
1286
|
+
exports.RXJS_HINTS = RXJS_HINTS;
|
|
1287
|
+
exports.checkEventBus = checkEventBus;
|
|
1288
|
+
exports.checks = checks;
|
|
1289
|
+
exports.configureChecks = configureChecks;
|
|
1290
|
+
exports.configureStrict = configureStrict;
|
|
1291
|
+
exports.getCodeInfo = getCodeInfo;
|
|
1292
|
+
exports.inspect = inspect;
|
|
1293
|
+
exports.installChecks = installChecks;
|
|
1294
|
+
exports.isStrictEnabled = isStrictEnabled;
|
|
1295
|
+
exports.listCodes = listCodes;
|
|
1296
|
+
exports.resetChecks = resetChecks;
|
|
1297
|
+
//# sourceMappingURL=diagnostics.cjs.js.map
|