sygnal 5.4.0 → 6.0.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/CHANGELOG.md +646 -0
- package/README.md +24 -9
- package/dist/astro/client.cjs.js +16 -9
- package/dist/astro/client.cjs.js.map +1 -1
- package/dist/astro/client.mjs +16 -9
- package/dist/astro/client.mjs.map +1 -1
- package/dist/astro/index.cjs.js +362 -91
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +362 -92
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +3396 -165
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3396 -165
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js +1431 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1419 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +3032 -296
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +3032 -296
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +257 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +381 -0
- package/dist/guide/forms.md +250 -0
- package/dist/guide/http.md +299 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +33 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +13178 -5769
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2908 -276
- package/dist/index.esm.js +13149 -5779
- package/dist/index.esm.js.map +1 -1
- package/dist/jsx-dev-runtime.cjs.js +26 -334
- package/dist/jsx-dev-runtime.cjs.js.map +1 -1
- package/dist/jsx-dev-runtime.esm.js +19 -327
- package/dist/jsx-dev-runtime.esm.js.map +1 -1
- package/dist/jsx-runtime.cjs.js +26 -334
- package/dist/jsx-runtime.cjs.js.map +1 -1
- package/dist/jsx-runtime.esm.js +19 -327
- package/dist/jsx-runtime.esm.js.map +1 -1
- package/dist/jsx.cjs.js +10 -314
- package/dist/jsx.cjs.js.map +1 -1
- package/dist/jsx.esm.js +8 -315
- package/dist/jsx.esm.js.map +1 -1
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +4 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -1
- package/dist/vike/ClientOnly.mjs +4 -14
- package/dist/vike/ClientOnly.mjs.map +1 -1
- package/dist/vike/{+config.js → config/+config.js} +9 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +165 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +165 -95
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/dist/vike/onRenderHtml.cjs.js +59 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -1
- package/dist/vike/onRenderHtml.mjs +60 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -1
- package/dist/vite/plugin.cjs.js +323 -88
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +323 -89
- package/dist/vite/plugin.mjs.map +1 -1
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +166 -103
- package/package.json +98 -14
- package/src/astro/client.ts +27 -16
- package/src/astro/index.d.ts +24 -0
- package/src/astro/index.ts +69 -3
- package/src/astro/server.ts +8 -2
- package/src/collection.ts +6 -93
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +9 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +9 -7
- package/src/cycle/dom/controlledInputModule.ts +23 -6
- package/src/cycle/dom/enrichEventStream.ts +25 -47
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -56
- package/src/cycle/dom/mockDOMSource.ts +21 -1
- package/src/cycle/dom/modules.ts +16 -3
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +19 -3
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +151 -0
- package/src/devtools.ts +34 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devtools.ts +101 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/collections.ts +1 -1
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +20 -11
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +17 -2
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +163 -0
- package/src/extra/diagnostics/checks/index.ts +97 -4
- package/src/extra/diagnostics/checks/inspect.ts +140 -32
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +10 -5
- package/src/extra/diagnostics/checks/public.d.ts +98 -7
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
- package/src/extra/diagnostics/checks/shared.ts +86 -2
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +100 -7
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +10 -67
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +66 -10
- package/src/extra/diagnostics/codes.ts +268 -23
- package/src/extra/diagnostics/index.ts +34 -9
- package/src/extra/diagnostics/legacy.ts +17 -0
- package/src/extra/driverFactories.ts +83 -19
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +268 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +1 -1
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +90 -217
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +2433 -175
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/index.d.ts +2631 -117
- package/src/index.ts +25 -4
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +256 -133
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +48 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -121
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +7 -1
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/plugin.d.ts +22 -3
- package/src/vite/plugin.ts +222 -24
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/vike/+config.cjs.js +0 -66
- package/dist/vike/+config.cjs.js.map +0 -1
- package/dist/vike/+config.js.map +0 -1
- package/src/component.ts +0 -2301
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -184
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -201
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- package/src/pragma/fn.ts +0 -55
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser sources (PLAN-5 B-3, src/extra/browserSources.ts). Dev-entry-only codes
|
|
3
|
+
* (DEV_CODE_SEVERITY), reported when the driver calls
|
|
4
|
+
* `__SYGNAL_DIAGNOSTICS__.browserSource(code, name, spec, componentName, extra)`:
|
|
5
|
+
*
|
|
6
|
+
* SYG663 — a `browser` entry the driver can't start (no known kind, no `action`, an intersection
|
|
7
|
+
* / resize target that is neither a selector nor `true`), or a BROWSER sink command
|
|
8
|
+
* with an unknown method. It isn't started / run.
|
|
9
|
+
* SYG664 — a kind the driver wasn't made with (`makeBrowserDriverWith(...)` without its source).
|
|
10
|
+
* SYG665 — a source or command failed (permission denied, API missing, storage blocked) and its
|
|
11
|
+
* spec names no `error` action, so nothing else would say so.
|
|
12
|
+
* SYG668 — an intersection / resize declaration with nothing to observe: no DOM source reached
|
|
13
|
+
* the driver for the instance (extra 'dom'), or, in renderComponent's fake with
|
|
14
|
+
* `dom: 'real'`, its selector matches no element of the component (extra 'none').
|
|
15
|
+
*
|
|
16
|
+
* A component declaring `browser` with no browser driver is SYG643 (checks/timers.ts).
|
|
17
|
+
*/
|
|
18
|
+
import {bridge, devReport, once} from './shared'
|
|
19
|
+
|
|
20
|
+
const KINDS = ['intersection', 'resize', 'media', 'storage', 'visibility', 'online', 'geolocation']
|
|
21
|
+
const SOURCE: Record<string, string> = {
|
|
22
|
+
intersection: 'intersectionSource', resize: 'resizeSource', media: 'mediaSource', storage: 'storageSource',
|
|
23
|
+
visibility: 'visibilitySource', online: 'onlineSource', geolocation: 'geolocationSource',
|
|
24
|
+
copy: 'clipboardSource', paste: 'clipboardSource', setItem: 'storageSource', removeItem: 'storageSource',
|
|
25
|
+
}
|
|
26
|
+
const brief = (v: any) => { try { return JSON.stringify(v) } catch (_) { return String(v) } }
|
|
27
|
+
|
|
28
|
+
/** why a declared spec can't start ('' when it can; mirrors browserDriver's checks) */
|
|
29
|
+
export function browserSpecProblem(s: any): string {
|
|
30
|
+
if (!s || typeof s != 'object') return `is not an object ({ ${KINDS.join(' | ')}: …, action })`
|
|
31
|
+
const k = KINDS.find(k => k in s)
|
|
32
|
+
if (!k) return `names no source (one of ${KINDS.join(', ')})`
|
|
33
|
+
if (!s.action || typeof s.action != 'string') return 'has no action (a string)'
|
|
34
|
+
if ((k == 'intersection' || k == 'resize') && s[k] !== true && (typeof s[k] != 'string' || !s[k])) return `${k} must be a selector in the component or true (its root element), got ${brief(s[k])}`
|
|
35
|
+
return ''
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function onBrowserSource(code: string, name: string, spec: any, component?: string, extra?: any, commands?: string[]) {
|
|
39
|
+
if (code == 'SYG663' && extra === 0) {
|
|
40
|
+
// an unknown command (extra 0; `commands`: the driver's)
|
|
41
|
+
if (!once(`SYG663:${component}:cmd:${name}`)) return
|
|
42
|
+
devReport('SYG663', {
|
|
43
|
+
component,
|
|
44
|
+
message: `BROWSER command '${name}' is unknown; it is not run`,
|
|
45
|
+
fix: `Name the method as a key of the command: ${(commands || []).map(c => `{ ${c}: … }`).join(', ') || 'this driver takes no commands'}`,
|
|
46
|
+
data: {name, command: spec},
|
|
47
|
+
})
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
if (code == 'SYG663') {
|
|
51
|
+
if (!once(`SYG663:${component}:${name}:${brief(spec)}`)) return
|
|
52
|
+
devReport('SYG663', {
|
|
53
|
+
component,
|
|
54
|
+
message: `browser entry '${name}' ${browserSpecProblem(spec) || 'is invalid'}; it is not started`,
|
|
55
|
+
fix: `Declare ${name}: { media: '(prefers-color-scheme: dark)', action: 'DARK' }, { intersection: '.card', action: 'SEEN' }, … (falsy to stop it)`,
|
|
56
|
+
data: {name, spec},
|
|
57
|
+
})
|
|
58
|
+
} else if (code == 'SYG664') {
|
|
59
|
+
if (!once(`SYG664:${component}:${name}`)) return
|
|
60
|
+
devReport('SYG664', {
|
|
61
|
+
component,
|
|
62
|
+
message: `browser entry '${name}' uses the ${extra} source, which this browser driver wasn't made with; it is not started`,
|
|
63
|
+
fix: `Add it: makeBrowserDriverWith(${SOURCE[extra]}, …), or use makeBrowserDriver() (every source)`,
|
|
64
|
+
data: {name, spec, kind: extra},
|
|
65
|
+
})
|
|
66
|
+
} else if (code == 'SYG668') {
|
|
67
|
+
if (!once(`SYG668:${component}:${name}:${extra}`)) return
|
|
68
|
+
const k = KINDS.find(k => spec && k in spec) || 'intersection', target = spec?.[k]
|
|
69
|
+
devReport('SYG668', {
|
|
70
|
+
component,
|
|
71
|
+
message: extra == 'none'
|
|
72
|
+
? `browser entry '${name}' observes ${brief(target)}, which matches no element of the component; it reports nothing`
|
|
73
|
+
: `browser entry '${name}' (${k}) has no DOM to observe: the component's DOM source didn't reach the browser driver; it reports nothing`,
|
|
74
|
+
fix: extra == 'none'
|
|
75
|
+
? `Use a selector the component's own view renders (or true for its root element), and declare the entry only while that element exists`
|
|
76
|
+
: `Run the app with a DOM driver (run() with a mount point) and declare intersection / resize in a component's own \`browser\` static`,
|
|
77
|
+
data: {name, spec, reason: extra},
|
|
78
|
+
})
|
|
79
|
+
} else if (code == 'SYG665') {
|
|
80
|
+
if (!once(`SYG665:${component}:${name}:${extra?.name ?? extra?.code}`)) return
|
|
81
|
+
devReport('SYG665', {
|
|
82
|
+
component,
|
|
83
|
+
message: `browser ${spec && 'action' in spec ? `entry '${name}'` : `command '${name}'`} failed (${extra?.message || extra?.name || 'error'}) and names no error action`,
|
|
84
|
+
fix: `Name one to handle it, e.g. { ..., error: 'FAILED' }, and a model entry FAILED: (state, { message }) => ...`,
|
|
85
|
+
data: {name, spec, failure: extra},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** install the hook the browser driver calls (removed by the returned function) */
|
|
91
|
+
export function installBrowserSourceHooks(): () => void {
|
|
92
|
+
const core = bridge()
|
|
93
|
+
if (!core) return () => {}
|
|
94
|
+
core.browserSource = onBrowserSource
|
|
95
|
+
return () => { if (core.browserSource === onBrowserSource) core.browserSource = undefined }
|
|
96
|
+
}
|
|
@@ -20,7 +20,7 @@ import {reportSafely, once, nameOf, isPlainObject} from './shared'
|
|
|
20
20
|
const describe = (v: any): string =>
|
|
21
21
|
v === null ? 'null' : v === undefined ? 'undefined' : Array.isArray(v) ? 'an array' : typeof v === 'object' ? 'an object' : `a ${typeof v}`
|
|
22
22
|
|
|
23
|
-
function checkCollection(component: any, el: any): void {
|
|
23
|
+
export function checkCollection(component: any, el: any): void {
|
|
24
24
|
const props = (el && el.data && el.data.props) || {}
|
|
25
25
|
const from = props.from
|
|
26
26
|
const of = props.of
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-4 CT-1, runtime control checks (dev entry only; the static ones are in sygnal-check).
|
|
3
|
+
*
|
|
4
|
+
* SYG124 (error): a component passed where a control or selector is expected, as in
|
|
5
|
+
* `DOM.click(TodoItem)` or `DOM.select(TodoItem)`. Mechanism: the `sources` hook wraps the
|
|
6
|
+
* component's DOM source in a Proxy; `select()` (also chained: `DOM.select('.x').select(C)`)
|
|
7
|
+
* and every event shorthand check their first argument. A function that isn't a control is a
|
|
8
|
+
* component. Reported once per component and argument; the call then goes on to the DOM
|
|
9
|
+
* source, whose select() still throws ("expects the argument to be a string").
|
|
10
|
+
*
|
|
11
|
+
* SYG125 (error): controls are elements, not components.
|
|
12
|
+
* - a control given `.intent`, `.model` or `.initialState` (reported when it first renders,
|
|
13
|
+
* the statics are set after controls() returns);
|
|
14
|
+
* - a spec object whose `vnode()` returns anything but one element vnode (text, nothing, an
|
|
15
|
+
* array, a component). With diagnostics on, the render then throws an Error with the
|
|
16
|
+
* SYG125 text, so the component's error boundary names the control instead of failing on
|
|
17
|
+
* the stamp.
|
|
18
|
+
* Mechanism: controls() (src/extra/controls.ts) calls `__SYGNAL_DIAGNOSTICS__.control(control,
|
|
19
|
+
* vnode)` on every render; installControlHooks() publishes it on the core bridge.
|
|
20
|
+
*
|
|
21
|
+
* The same hook remembers which control rendered each vnode (controlOfVnode), so inspect() can
|
|
22
|
+
* list a component's controls with their element or kind.
|
|
23
|
+
*/
|
|
24
|
+
import type {DiagnosticCheck} from '../index'
|
|
25
|
+
import {bridge, devReport, once, onReset, nameOf, report} from './shared'
|
|
26
|
+
import {DEV_CODE_SEVERITY} from '../codes'
|
|
27
|
+
|
|
28
|
+
/** the key of a control, from its selector `[data-control="<Key>"]` */
|
|
29
|
+
export const controlKey = (control: any): string => String(control).slice(15, -2)
|
|
30
|
+
|
|
31
|
+
/** a control selector `[data-control="<Key>"]` as its key, else undefined */
|
|
32
|
+
export const keyOfSelector = (selector: string): string | undefined => {
|
|
33
|
+
const m = /^\[data-control="([^"]+)"\]$/.exec(selector)
|
|
34
|
+
return m ? m[1] : undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isControl = (x: any): boolean => typeof x == 'function' && !!x.__sygnalControl
|
|
38
|
+
|
|
39
|
+
const componentName = (fn: any): string =>
|
|
40
|
+
fn.componentName || fn.label || fn.name || 'the component'
|
|
41
|
+
|
|
42
|
+
let vnodeControls = new WeakMap<object, any>()
|
|
43
|
+
let staticsSeen = new WeakSet<object>()
|
|
44
|
+
onReset(() => {
|
|
45
|
+
vnodeControls = new WeakMap()
|
|
46
|
+
staticsSeen = new WeakSet()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/** the control that rendered a vnode (only while the dev entry is installed) */
|
|
50
|
+
export const controlOfVnode = (vnode: any): any =>
|
|
51
|
+
vnode && typeof vnode == 'object' ? vnodeControls.get(vnode) : undefined
|
|
52
|
+
|
|
53
|
+
const STATICS = ['intent', 'model', 'initialState']
|
|
54
|
+
|
|
55
|
+
const describe = (v: any): string =>
|
|
56
|
+
v === undefined || v === null ? String(v)
|
|
57
|
+
: Array.isArray(v) ? `an array of ${v.length}`
|
|
58
|
+
: typeof v != 'object' ? `a ${typeof v} (${JSON.stringify(v)})`
|
|
59
|
+
: typeof v.sel != 'string' ? (v.text !== undefined ? 'a text vnode' : 'a fragment')
|
|
60
|
+
// the pragma's component placeholder (the component function in data.c)
|
|
61
|
+
: v.data && typeof v.data.c == 'function' ? `the component <${v.sel}>`
|
|
62
|
+
: 'an element vnode'
|
|
63
|
+
|
|
64
|
+
function onControl(control: any, vnode: any): void {
|
|
65
|
+
const key = controlKey(control)
|
|
66
|
+
if (vnode && typeof vnode == 'object') vnodeControls.set(vnode, control)
|
|
67
|
+
|
|
68
|
+
if (!staticsSeen.has(control)) {
|
|
69
|
+
staticsSeen.add(control)
|
|
70
|
+
const given = STATICS.filter(s => control[s] != null)
|
|
71
|
+
if (given.length) {
|
|
72
|
+
devReport('SYG125', {
|
|
73
|
+
message: `The control ${key} has ${given.map(s => '.' + s).join(', ')}, but controls are elements, not components: ${given.length > 1 ? 'these statics are' : 'it is'} never used`,
|
|
74
|
+
fix: `Put the intent and model on the component that renders <${key}>, and listen to the control in its intent: DOM.click(${key}). For something with its own state and actions, write a component instead`,
|
|
75
|
+
data: {control: key, statics: given},
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const what = describe(vnode)
|
|
81
|
+
if (what == 'an element vnode') return
|
|
82
|
+
const kind = control.kind
|
|
83
|
+
// report() (not devReport): in 'error' mode its DiagnosticError is thrown into the render
|
|
84
|
+
const d = report('SYG125', {
|
|
85
|
+
severity: DEV_CODE_SEVERITY.SYG125,
|
|
86
|
+
message: `The control ${key}'s spec${kind ? ` (kind '${kind}')` : ''} vnode() returned ${what}, but it must return one element vnode`,
|
|
87
|
+
fix: `Return a single element built with the h argument: vnode(props, children, h) { return h('div', props, ...children) }`,
|
|
88
|
+
data: {control: key, kind, returned: what},
|
|
89
|
+
})
|
|
90
|
+
if (d) throw new Error(d.text)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Publish the control hook on the core bridge. Returns an uninstall function. */
|
|
94
|
+
export function installControlHooks(): () => void {
|
|
95
|
+
const core = bridge()
|
|
96
|
+
if (!core) return () => {}
|
|
97
|
+
core.control = onControl
|
|
98
|
+
return () => { if (core.control === onControl) core.control = undefined }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const wrapped = new WeakMap<object, any>()
|
|
102
|
+
|
|
103
|
+
/** a DOM source whose select() and event shorthands report SYG124 for a component argument */
|
|
104
|
+
function checkedDOM(component: any, dom: any): any {
|
|
105
|
+
if (!dom || typeof dom != 'object') return dom
|
|
106
|
+
let proxy = wrapped.get(dom)
|
|
107
|
+
if (proxy) return proxy
|
|
108
|
+
proxy = new Proxy(dom, {
|
|
109
|
+
get(t: any, k: any) {
|
|
110
|
+
const v = t[k]
|
|
111
|
+
if (typeof k != 'string' || typeof v != 'function' || (k != 'select' && k in t)) return v
|
|
112
|
+
return function (this: any, arg: any, ...rest: any[]) {
|
|
113
|
+
if (typeof arg == 'function' && !isControl(arg)) {
|
|
114
|
+
const name = nameOf(component), child = componentName(arg)
|
|
115
|
+
if (once(`SYG124:${name}:${child}:${k}`)) {
|
|
116
|
+
const call = k == 'select' ? `DOM.select(${child})` : `DOM.${k}(${child})`
|
|
117
|
+
devReport('SYG124', {
|
|
118
|
+
component,
|
|
119
|
+
message: `${name}.intent passes the component ${child} to ${call}, but a component is not an element: it has no element of its own to listen on, and its elements are isolated from ${name}`,
|
|
120
|
+
fix: `Handle the event in ${child} and send it up with PARENT, then read it here with CHILD.select(${child}); or render a control ${name} owns around it: const { ${child}Box } = controls({ ${child}Box: 'div' }), <${child}Box><${child} /></${child}Box>, DOM.click(${child}Box)`,
|
|
121
|
+
data: {component: child, method: k},
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const out = v.call(t, arg, ...rest)
|
|
126
|
+
return k == 'select' ? checkedDOM(component, out) : out
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
wrapped.set(dom, proxy)
|
|
131
|
+
return proxy
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const controlsCheck: DiagnosticCheck = {
|
|
135
|
+
id: 'controls',
|
|
136
|
+
|
|
137
|
+
sources(component, sources) {
|
|
138
|
+
if (!sources || typeof Proxy != 'function') return
|
|
139
|
+
const domName = component?.DOMSourceName || 'DOM'
|
|
140
|
+
if (!sources[domName]) return
|
|
141
|
+
return new Proxy(sources, {
|
|
142
|
+
get(t: any, k: any) {
|
|
143
|
+
const v = t[k]
|
|
144
|
+
return k === domName ? checkedDOM(component, v) : v
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
},
|
|
148
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SYG421 — invalid `data` (dataset) key in a view (error, G-143).
|
|
3
|
+
*
|
|
4
|
+
* JSX `data={{ ... }}` becomes the vnode's `data.dataset` (a `data-x-y="..."` attribute is
|
|
5
|
+
* camelCased to 'xY' by the pragma, G-152), which the DOM driver writes with
|
|
6
|
+
* `element.dataset[key] = value`. The browser throws a SyntaxError DOMException for a key with
|
|
7
|
+
* a hyphen followed by a lower-case letter (`'task-id'`), and setAttribute() throws for a name
|
|
8
|
+
* with characters an attribute can't have (spaces, quotes, `=`, ...). The patch fails, so the
|
|
9
|
+
* app stops rendering, and the console shows only a bare "DOMException {}".
|
|
10
|
+
*
|
|
11
|
+
* Mechanism: onRender (before the DOM driver patches) walks the rendered vnode tree and checks
|
|
12
|
+
* every dataset key. Reported once per key; the mock DOM of renderComponent never throws, so
|
|
13
|
+
* this is also the only signal there.
|
|
14
|
+
*/
|
|
15
|
+
import type {DiagnosticCheck} from '../index'
|
|
16
|
+
import {devReport, once, nameOf} from './shared'
|
|
17
|
+
|
|
18
|
+
const camel = (key: string): string => key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
19
|
+
const BAD_CHAR = /[\s"'<>\/=`]/
|
|
20
|
+
|
|
21
|
+
function problem(key: string): string | undefined {
|
|
22
|
+
if (/-[a-z]/.test(key)) return `has a hyphen followed by a lower-case letter, which a dataset name can't have`
|
|
23
|
+
if (BAD_CHAR.test(key)) return `contains a character an HTML attribute name can't have`
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function walk(vnode: any, visit: (dataset: Record<string, any>, sel: string) => void, depth = 0): void {
|
|
28
|
+
if (!vnode || typeof vnode != 'object' || depth > 500) return
|
|
29
|
+
const ds = vnode.data && vnode.data.dataset
|
|
30
|
+
if (ds && typeof ds == 'object') visit(ds, vnode.sel)
|
|
31
|
+
const children = vnode.children
|
|
32
|
+
if (Array.isArray(children)) for (const c of children) walk(c, visit, depth + 1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const datasetCheck: DiagnosticCheck = {
|
|
36
|
+
id: 'dataset',
|
|
37
|
+
|
|
38
|
+
onRender(component, rootVnode) {
|
|
39
|
+
walk(rootVnode, (dataset, sel) => {
|
|
40
|
+
for (const key of Object.keys(dataset)) {
|
|
41
|
+
const why = problem(key)
|
|
42
|
+
if (!why || !once(`SYG421:${key}`)) continue
|
|
43
|
+
const good = camel(key).replace(new RegExp(BAD_CHAR.source, 'g'), '')
|
|
44
|
+
const attr = good.replace(/[A-Z]/g, c => '-' + c.toLowerCase())
|
|
45
|
+
const name = good || 'name'
|
|
46
|
+
devReport('SYG421', {
|
|
47
|
+
component,
|
|
48
|
+
message: `The view of ${nameOf(component)} renders the data key '${key}'${sel ? ` on <${String(sel).split(/[.#]/)[0]}>` : ''} ` +
|
|
49
|
+
`(from data={{ '${key}': … }}), but '${key}' ${why}. ` +
|
|
50
|
+
`Writing it throws a DOMException, which can stop rendering`,
|
|
51
|
+
fix: `Use a camelCase key in the data prop: data={{ ${name}: … }} renders the attribute data-${attr || 'name'}; read it with .data('${name}')`,
|
|
52
|
+
data: {key, suggested: good, attribute: `data-${attr}`},
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
}
|
|
@@ -37,6 +37,7 @@ import type {DiagnosticCheck} from '../index'
|
|
|
37
37
|
import {ScopeChecker} from '../../../cycle/dom/ScopeChecker'
|
|
38
38
|
import type {Scope} from '../../../cycle/dom/isolate'
|
|
39
39
|
import {reportSafely, once, onReset, timing, nameOf} from './shared'
|
|
40
|
+
import {keyOfSelector} from './controls'
|
|
40
41
|
|
|
41
42
|
interface Tracked {
|
|
42
43
|
name: string
|
|
@@ -83,7 +84,9 @@ function viewSource(component: any): string {
|
|
|
83
84
|
|
|
84
85
|
/** class / id / tag tokens of a selector: '.a .b-c > #d' -> ['a', 'b-c', 'd'] */
|
|
85
86
|
const tokensOf = (selector: string): string[] =>
|
|
86
|
-
(selector.replace(/\[[^\]]*\]|:[\w-]+(\([^)]*\))?/g, ' ').match(/[A-Za-z_][\w-]*/g) || [])
|
|
87
|
+
(selector.replace(/\[[^\]]*\]|:[\w-]+(\([^)]*\))?/g, ' ').match(/[A-Za-z_][\w-]*/g) || [] as string[])
|
|
88
|
+
// CT-1: a control's key (`<Add>` / `h(Add)` in the view source)
|
|
89
|
+
.concat(Array.from(selector.matchAll(/\[data-control="([^"]+)"\]/g), m => m[1]))
|
|
87
90
|
|
|
88
91
|
function appearsInView(component: any, selector: string): boolean {
|
|
89
92
|
const src = viewSource(component)
|
|
@@ -93,12 +96,13 @@ function appearsInView(component: any, selector: string): boolean {
|
|
|
93
96
|
return tokens.every(t => new RegExp(`(^|[^\\w-])${t.replace(/[-]/g, '\\-')}($|[^\\w-])`).test(src))
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
// G-144: a fragment-rooted component has several root elements
|
|
100
|
+
function rootElementsOf(t: Tracked): Element[] {
|
|
97
101
|
try {
|
|
98
102
|
const im = t.source._isolateModule
|
|
99
|
-
return im && im.
|
|
103
|
+
return (im && im.getElements(scopesOf(t.source.namespace))) || []
|
|
100
104
|
} catch (_) {
|
|
101
|
-
return
|
|
105
|
+
return []
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
|
|
@@ -133,8 +137,8 @@ function report103(component: any, t: Tracked, selector: string, severity: 'info
|
|
|
133
137
|
|
|
134
138
|
function sweep(component: any, t: Tracked, escalate: boolean): void {
|
|
135
139
|
if (t.disposed) return
|
|
136
|
-
const
|
|
137
|
-
if (!
|
|
140
|
+
const roots = rootElementsOf(t).filter(r => typeof r.querySelectorAll === 'function')
|
|
141
|
+
if (!roots.length) return
|
|
138
142
|
const im = t.source._isolateModule
|
|
139
143
|
const checker = new ScopeChecker(t.source.namespace, im)
|
|
140
144
|
let unmatched = false
|
|
@@ -144,8 +148,11 @@ function sweep(component: any, t: Tracked, escalate: boolean): void {
|
|
|
144
148
|
if (matched.has(matchKey) || t.crossed.has(selector)) continue
|
|
145
149
|
let all: Element[]
|
|
146
150
|
try {
|
|
147
|
-
all =
|
|
148
|
-
|
|
151
|
+
all = []
|
|
152
|
+
for (const root of roots) {
|
|
153
|
+
all.push(...Array.prototype.slice.call(root.querySelectorAll(selector)))
|
|
154
|
+
if (root.matches(selector)) all.push(root)
|
|
155
|
+
}
|
|
149
156
|
} catch (_) {
|
|
150
157
|
continue // invalid selector: the DOM driver reports that itself
|
|
151
158
|
}
|
|
@@ -156,11 +163,13 @@ function sweep(component: any, t: Tracked, escalate: boolean): void {
|
|
|
156
163
|
t.crossed.add(selector)
|
|
157
164
|
if (!once(`SYG104:${t.name}:${selector}`)) continue
|
|
158
165
|
const child = childNameFor(t, all) || 'a child component'
|
|
166
|
+
// CT-1: a control is named by its identifier
|
|
167
|
+
const control = keyOfSelector(selector)
|
|
159
168
|
reportSafely('SYG104', {
|
|
160
169
|
component,
|
|
161
|
-
message: `DOM.select('${selector}') in ${t.name} matches elements inside ${child} (isolated), so ${t.name} never receives their events`,
|
|
162
|
-
fix: `Handle the event in ${child} and send it up with PARENT (read it here with CHILD.select(${child})), or use EVENTS`,
|
|
163
|
-
data: {selector, child},
|
|
170
|
+
message: `${control ? `The control ${control}` : `DOM.select('${selector}')`} in ${t.name} matches elements inside ${child} (isolated), so ${t.name} never receives their events`,
|
|
171
|
+
fix: `Handle the event in ${child}${control ? ` (DOM.<event>(${control}) in its intent)` : ''} and send it up with PARENT (read it here with CHILD.select(${child})), or use EVENTS`,
|
|
172
|
+
data: control ? {selector, child, control} : {selector, child},
|
|
164
173
|
})
|
|
165
174
|
continue
|
|
166
175
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-4 GS-2: element command diagnostics (dev entry; the core carries no text).
|
|
3
|
+
*
|
|
4
|
+
* The core (src/extra/elementCommands.ts) calls `__SYGNAL_DIAGNOSTICS__.elementCommand(component,
|
|
5
|
+
* command, element?)` when a command can't run:
|
|
6
|
+
* - no element (`element` falsy): SYG640 (warn): nothing in the sending instance's DOM scope
|
|
7
|
+
* matched the target within about 1 s;
|
|
8
|
+
* - an element, but neither the control's spec command nor a method of the element: SYG641
|
|
9
|
+
* (error). It names the control's declared commands. When the element is a widget's host
|
|
10
|
+
* (PLAN-5 W-1, `__sw`): SYG142 (error), naming the widget's declared commands.
|
|
11
|
+
* installElementCommandHooks() publishes it on the core bridge.
|
|
12
|
+
*
|
|
13
|
+
* When a command is sent (checkSentCommand, from the elementCommands check's onModel, which wraps
|
|
14
|
+
* the instance's ELEMENT stream before the core subscribes it): SYG641 for a value that is not a
|
|
15
|
+
* command object, and for a method that changes the DOM Sygnal renders (remove, append,
|
|
16
|
+
* setAttribute...): the core runs any method the element has, and those break the next patch.
|
|
17
|
+
*
|
|
18
|
+
* renderComponent uses these functions too (mock DOM, and the real DOM without the dev entry), so
|
|
19
|
+
* this module has no side effects and doesn't import ./shared (which registers codes when loaded).
|
|
20
|
+
*/
|
|
21
|
+
import type {DiagnosticCheck} from '../index'
|
|
22
|
+
import {DEV_CODE_SEVERITY} from '../codes'
|
|
23
|
+
|
|
24
|
+
/** The element commands Sygnal documents and types (the core runs any method of the element) */
|
|
25
|
+
export const NATIVE_COMMAND_NAMES = ['focus', 'blur', 'select', 'click', 'scrollIntoView', 'showModal', 'show', 'close', 'showPopover', 'hidePopover', 'togglePopover']
|
|
26
|
+
|
|
27
|
+
const core = (): any => (globalThis as any).__SYGNAL_DIAGNOSTICS__
|
|
28
|
+
|
|
29
|
+
const send = (code: string, details: any): void => {
|
|
30
|
+
try {
|
|
31
|
+
core()?.report(code, {severity: DEV_CODE_SEVERITY[code], ...details})
|
|
32
|
+
} catch (err) {
|
|
33
|
+
// 'error' mode: rethrown asynchronously (never into the caller)
|
|
34
|
+
setTimeout(() => { throw err })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isControl = (x: any): boolean => typeof x == 'function' && !!x.__sygnalControl
|
|
39
|
+
const keyOf = (control: any): string => String(control).slice(15, -2)
|
|
40
|
+
|
|
41
|
+
/** a command as written: `{ focus: Email, preventScroll: true }` */
|
|
42
|
+
export const describeCommand = (cmd: any): string => {
|
|
43
|
+
if (!cmd || typeof cmd != 'object') return JSON.stringify(cmd) ?? String(cmd)
|
|
44
|
+
const parts = Object.keys(cmd).map(k => {
|
|
45
|
+
const v = cmd[k]
|
|
46
|
+
let text: string
|
|
47
|
+
if (isControl(v)) text = keyOf(v)
|
|
48
|
+
else try { text = JSON.stringify(v) ?? String(v) } catch (_) { text = String(v) }
|
|
49
|
+
return `${k}: ${text}`
|
|
50
|
+
})
|
|
51
|
+
return `{ ${parts.join(', ')} }`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const distance = (a: string, b: string): number => {
|
|
55
|
+
const row = Array.from({length: b.length + 1}, (_, i) => i)
|
|
56
|
+
for (let i = 1; i <= a.length; i++) {
|
|
57
|
+
let prev = row[0]
|
|
58
|
+
row[0] = i
|
|
59
|
+
for (let j = 1; j <= b.length; j++) {
|
|
60
|
+
const cur = row[j]
|
|
61
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] == b[j - 1] ? 0 : 1))
|
|
62
|
+
prev = cur
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return row[b.length]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const closest = (name: string, names: string[]): string | undefined => {
|
|
69
|
+
let best: string | undefined, d = 3
|
|
70
|
+
for (const n of names) {
|
|
71
|
+
const x = n.toLowerCase() == name.toLowerCase() ? 0 : distance(name, n)
|
|
72
|
+
if (x < d) { d = x; best = n }
|
|
73
|
+
}
|
|
74
|
+
return best
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const tagOf = (el: any): string => el && typeof el.tagName == 'string' ? `<${el.tagName.toLowerCase()}>` : 'element'
|
|
78
|
+
|
|
79
|
+
const notACommand = (component: any, name: string, cmd: any) => send('SYG641', {
|
|
80
|
+
component,
|
|
81
|
+
message: `${name} sent ${describeCommand(cmd)} to the ELEMENT sink, which is not a command`,
|
|
82
|
+
fix: `Send { <method>: control or selector, ...options }, e.g. { focus: Email }, or an array of them`,
|
|
83
|
+
data: {command: cmd},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/** SYG640 (no `el`) or SYG641 (an element, but no command of that name) for `cmd` sent by `component` */
|
|
87
|
+
export function reportElementCommand(component: any, cmd: any, el?: any): void {
|
|
88
|
+
// a disposed instance's pending commands are dropped silently
|
|
89
|
+
if (component && component._disposed) return
|
|
90
|
+
const name = (component && component.name) || 'component'
|
|
91
|
+
const method = cmd && typeof cmd == 'object' ? Object.keys(cmd)[0] : undefined
|
|
92
|
+
if (method === undefined) return notACommand(component, name, cmd)
|
|
93
|
+
const target = cmd[method]
|
|
94
|
+
const what = `ELEMENT ${describeCommand(cmd)}`
|
|
95
|
+
const control = isControl(target) ? keyOf(target) : undefined
|
|
96
|
+
const data: any = {method, command: cmd, ...(control ? {control} : {selector: target == null ? target : String(target)})}
|
|
97
|
+
if (!el) {
|
|
98
|
+
const missing = target == null || target === ''
|
|
99
|
+
return send('SYG640', {
|
|
100
|
+
component,
|
|
101
|
+
message: missing
|
|
102
|
+
? `${what} in ${name} has no target (${String(target)}), so the command was dropped`
|
|
103
|
+
: `${what} in ${name} matched no element: nothing in ${name}'s own view matches ${control ? `the control ${control}` : `'${String(target)}'`} (waited 1 s), so the command was dropped`,
|
|
104
|
+
fix: missing
|
|
105
|
+
? `Give the method a control or a selector: { ${method}: Email }`
|
|
106
|
+
: `Render the element in ${name}'s view, or send the command from the component that renders it: a child component's elements are isolated from its parent, and a command reaches only its sender's own elements (in a Collection, each item sends its own)`,
|
|
107
|
+
data,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
// PLAN-5 W-1: the matched element is a widget's host (selector or control target): SYG142 names
|
|
111
|
+
// the widget's declared commands
|
|
112
|
+
const widget = el.__sw?.w
|
|
113
|
+
if (widget) {
|
|
114
|
+
const declared = Object.keys(widget.commands)
|
|
115
|
+
const hint = closest(method, [...declared, ...NATIVE_COMMAND_NAMES])
|
|
116
|
+
return send('SYG142', {
|
|
117
|
+
component,
|
|
118
|
+
message: `${what} in ${name}: the matched ${tagOf(el)} is a widget's host, and the widget declares no '${method}' command` +
|
|
119
|
+
(hint ? ` (did you mean '${hint}'?)` : '') + (declared.length ? `; it declares: ${declared.join(', ')}` : '; it declares none'),
|
|
120
|
+
fix: `Add '${method}' to the widget's commands (defineWidget({ commands: { ${method}: (instance, options) => … } })), or send one it declares`,
|
|
121
|
+
data: {...data, element: tagOf(el), commands: declared},
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
const commands = control && target.spec && typeof target.spec == 'object' && target.spec.commands
|
|
125
|
+
? Object.keys(target.spec.commands) : []
|
|
126
|
+
data.element = tagOf(el)
|
|
127
|
+
if (commands.length) data.commands = commands
|
|
128
|
+
let message: string, fix: string
|
|
129
|
+
if (NATIVE_COMMAND_NAMES.includes(method)) {
|
|
130
|
+
message = `${what} in ${name}: the matched ${tagOf(el)} has no ${method}() method${commands.length ? `, and the control ${control}'s spec declares no '${method}' command (it declares: ${commands.join(', ')})` : ''}`
|
|
131
|
+
fix = /^(show|showModal|close)$/.test(method)
|
|
132
|
+
? `${method}() works on a <dialog> element: render the target as a <dialog>`
|
|
133
|
+
: /Popover$/.test(method)
|
|
134
|
+
? `The popover methods need an element with the popover attribute (attrs: { popover: 'auto' }) in a browser with the Popover API`
|
|
135
|
+
: `Target an element that has ${method}()`
|
|
136
|
+
} else {
|
|
137
|
+
const hint = closest(method, [...commands, ...NATIVE_COMMAND_NAMES])
|
|
138
|
+
message = `${what} in ${name}: '${method}' is not an element command` + (hint ? ` (did you mean '${hint}'?)` : '') +
|
|
139
|
+
(el.tagName ? `: the matched ${tagOf(el)} has no such method` : '') +
|
|
140
|
+
(commands.length ? `, and the control ${control}'s spec declares only: ${commands.join(', ')}` : '')
|
|
141
|
+
fix = `Use ${commands.length ? `one of its commands (${commands.join(', ')}) or ` : ''}one of: ${NATIVE_COMMAND_NAMES.join(', ')} (or another method of the element, like play). The first key of a command is the method, the others are its options: { scrollIntoView: Row, block: 'nearest' }`
|
|
142
|
+
}
|
|
143
|
+
send('SYG641', {component, message, fix, data})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// methods that change the DOM snabbdom owns (the next patch undoes them or trips over them)
|
|
147
|
+
const DOM_MUTATORS = /^(remove|append|prepend|before|after|replaceWith|replaceChildren|appendChild|removeChild|insertBefore|replaceChild|insertAdjacent(Element|HTML|Text)|(set|remove|toggle)Attribute(NS|Node)?|attachShadow|setHTML(Unsafe)?|normalize)$/
|
|
148
|
+
|
|
149
|
+
/** SYG641 when it is sent: not a command object, or a method that changes the DOM Sygnal renders */
|
|
150
|
+
export function checkSentCommand(component: any, cmd: any): void {
|
|
151
|
+
if (cmd == null) return
|
|
152
|
+
const name = (component && component.name) || 'component'
|
|
153
|
+
if (typeof cmd != 'object' || Array.isArray(cmd) || !Object.keys(cmd).length) return notACommand(component, name, cmd)
|
|
154
|
+
const method = Object.keys(cmd)[0], target = cmd[method]
|
|
155
|
+
if (!DOM_MUTATORS.test(method) || (isControl(target) && target.spec?.commands?.[method])) return
|
|
156
|
+
send('SYG641', {
|
|
157
|
+
component,
|
|
158
|
+
message: `ELEMENT ${describeCommand(cmd)} in ${name}: ${method}() changes the DOM that ${name}'s view renders, so the next render undoes it or fails`,
|
|
159
|
+
fix: `Change the state and render the result instead; element commands are for what a view can't express (focus, scrolling, dialogs, popovers, media)`,
|
|
160
|
+
data: {method, command: cmd},
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The dev entry check: checkSentCommand runs on every ELEMENT value through the core's onElementCommand hook (checks/next.ts) */
|
|
165
|
+
export const elementCommandsCheck: DiagnosticCheck = {
|
|
166
|
+
id: 'elementCommands',
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Publish the element command hook on the core bridge. Returns an uninstall function. */
|
|
170
|
+
export function installElementCommandHooks(): () => void {
|
|
171
|
+
const c = core()
|
|
172
|
+
if (!c) return () => {}
|
|
173
|
+
c.elementCommand = reportElementCommand
|
|
174
|
+
return () => { if (c.elementCommand === reportElementCommand) c.elementCommand = undefined }
|
|
175
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SYG105 — EVENTS type emitted but never selected, or selected but never
|
|
3
3
|
* emitted (info).
|
|
4
|
+
* SYG116 — a value with no string `type` (typically a function) reached the
|
|
5
|
+
* EVENTS bus (error, G-143).
|
|
4
6
|
*
|
|
5
7
|
* Mechanism: the EVENTS driver (src/extra/eventDriver.ts) calls onBusSelect
|
|
6
8
|
* for every EVENTS.select(type) and onBusEmit for every bus event.
|
|
@@ -12,7 +14,7 @@
|
|
|
12
14
|
* EVENTS.select() with no type (all events) counts as selecting everything.
|
|
13
15
|
*/
|
|
14
16
|
import type {DiagnosticCheck} from '../index'
|
|
15
|
-
import {report, once, onReset, didYouMean} from './shared'
|
|
17
|
+
import {report, devReport, once, onReset, didYouMean} from './shared'
|
|
16
18
|
|
|
17
19
|
let selected = new Set<string>()
|
|
18
20
|
let emitted = new Set<string>()
|
|
@@ -29,7 +31,20 @@ export const eventsCheck: DiagnosticCheck = {
|
|
|
29
31
|
},
|
|
30
32
|
|
|
31
33
|
onBusEmit(type, emitterName) {
|
|
32
|
-
if (typeof type !== 'string')
|
|
34
|
+
if (typeof type !== 'string') {
|
|
35
|
+
// SYG116 (G-143): no EVENTS.select() can ever receive it. The core copies each emitted
|
|
36
|
+
// value into a new object (devtools stamp), so a function arrives here as {}.
|
|
37
|
+
if (once(`SYG116:${emitterName}`)) {
|
|
38
|
+
devReport('SYG116', {
|
|
39
|
+
component: emitterName,
|
|
40
|
+
message: `${emitterName ? emitterName + "'s model" : 'A model'} sent a value with no string 'type' to the EVENTS sink, so nothing receives it. ` +
|
|
41
|
+
`The value was probably a function (a model entry that returns event(...) instead of being event(...)), or undefined/null`,
|
|
42
|
+
fix: `Make event() the sink entry itself: ACTION: { EVENTS: event('TYPE', (state, data) => payload) }; return ABORT to send nothing`,
|
|
43
|
+
data: {type, emitter: emitterName},
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
return
|
|
47
|
+
}
|
|
33
48
|
emitted.add(type)
|
|
34
49
|
if (selectsAll || selected.has(type) || !once(`SYG105:emit:${type}`)) return
|
|
35
50
|
report('SYG105', {
|