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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-5 B-1 (3-D): `sortable` behavior diagnostics (dev entry; src/extra/sortable.ts carries no
|
|
3
|
+
* text). The behavior calls `__SYGNAL_DIAGNOSTICS__.sortable(code, ...args)`:
|
|
4
|
+
* 145 (item element, item selector, attr) a press or key on an item whose element has no
|
|
5
|
+
* `attr`: SYG145 (warn, once per selector); the event is ignored
|
|
6
|
+
* 146 (host root, item, handle, attr) the host's first pointerdown / keydown: SYG146 (warn,
|
|
7
|
+
* once per selector) when elements carry `attr` under the root but none matches `item`,
|
|
8
|
+
* or items exist but none contains `handle`. Returns true once it could decide (an empty
|
|
9
|
+
* list can't tell: the check runs again at the next interaction)
|
|
10
|
+
* 147 (from key, host state, uses key) when the host starts: a `from` key whose value isn't
|
|
11
|
+
* an array: SYG147 (warn, once per key)
|
|
12
|
+
* 435 (3-H G-449) (fn, uses key, from keys, idOf): the host's own items' ids as shown, per
|
|
13
|
+
* container (fn() returns [the item elements, the root]; the behavior only asks in dev), or nothing when
|
|
14
|
+
* every list has been reported (3-L G-478: no scan). A row's place is its `data-index`
|
|
15
|
+
* when it has one (VirtualCollection's pinned focused row is out of DOM order). (shown
|
|
16
|
+
* groups, host state, from keys, idField, uses key) at a keyboard pick-up or a pointer
|
|
17
|
+
* press: SYG435 (warn, once per uses key and list) when a list's shown items (those in
|
|
18
|
+
* the container holding most of them: an element elsewhere with a matching id is no list
|
|
19
|
+
* item; a row's container is its nearest ancestor holding another row, 3-O G-505) aren't
|
|
20
|
+
* one run of the array in its order (a Collection's sort, or a filter hiding entries
|
|
21
|
+
* between shown ones)
|
|
22
|
+
*/
|
|
23
|
+
import {devReport, once, onced, suggest} from './shared'
|
|
24
|
+
|
|
25
|
+
const all = (root: any, sel: string): any[] => {
|
|
26
|
+
try { return [...root.querySelectorAll(sel)] } catch (_) { return [] }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function reportSortable(code: number, a?: any, b?: any, c?: any, d?: any, e?: any): any {
|
|
30
|
+
if (code == 145) {
|
|
31
|
+
if (once(`SYG145:${b}:${c}`)) devReport('SYG145', {
|
|
32
|
+
component: 'sortable',
|
|
33
|
+
message: `sortable: an item ('${b}') has no ${c} attribute, so it can't be moved (the behavior finds each item in the list by that id)`,
|
|
34
|
+
fix: `Render the item's id on its element: <li className="${String(b).replace(/^\./, '')}" ${c}={item.id}>, or name the attribute: sortable({ ..., attr: 'data-key' })`,
|
|
35
|
+
data: {item: b, attr: c},
|
|
36
|
+
})
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
if (code == 146) {
|
|
40
|
+
const root = a, item = b, handle = c, attr = d
|
|
41
|
+
if (!root?.querySelectorAll) return true
|
|
42
|
+
const items = all(root, item).filter(el => el !== root)
|
|
43
|
+
if (!items.length) {
|
|
44
|
+
// nothing carries the id attribute either: an empty list (decide later)
|
|
45
|
+
if (!all(root, `[${attr}]`).length) return false
|
|
46
|
+
report146('item', item, `no element under the host matches it, though elements with ${attr} are rendered`, `Use the selector of the items' elements (the element with ${attr}), e.g. item: '.task'`)
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
if (handle && !items.some(el => all(el, handle).length)) {
|
|
50
|
+
report146('handle', handle, `no item ('${item}') contains an element that matches it`, `Use the selector of the drag handle inside each item, e.g. handle: '.grip', or leave handle out to drag the whole item`)
|
|
51
|
+
}
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
if (code == 435) {
|
|
55
|
+
// (fn, uses key, from keys, idOf) the shown ids per container: computed only here (dev)
|
|
56
|
+
if (typeof a == 'function') {
|
|
57
|
+
if ((c || []).every((l: string) => onced(`SYG435:${b}:${l}`))) return
|
|
58
|
+
try {
|
|
59
|
+
const by = new Map<any, any[]>(), [els, root] = a(), n = new Map<any, number>()
|
|
60
|
+
// a row's place: its data-index (a VirtualCollection row below the host's root; its
|
|
61
|
+
// pinned row isn't in DOM order)
|
|
62
|
+
const rows = els.map((el: any) => {
|
|
63
|
+
const r0 = el.closest?.('[data-index]'), row = r0 && r0 !== root && root.contains?.(r0) ? r0 : el
|
|
64
|
+
return [el, row, Number(row === el && !r0 ? NaN : row.getAttribute('data-index'))]
|
|
65
|
+
})
|
|
66
|
+
// 3-O G-505: a row's container is its nearest ancestor holding another row (items each in
|
|
67
|
+
// their own wrapper, <li><div data-id>, a table cell, a Collection item's root, are still
|
|
68
|
+
// one list); `n` counts the rows below each ancestor
|
|
69
|
+
const up = (row: any, f: (p: any) => any) => { for (let p = row.parentElement; p; p = p === root ? null : p.parentElement) if (f(p)) return p }
|
|
70
|
+
for (const [, row] of rows) up(row, p => void n.set(p, (n.get(p) || 0) + 1))
|
|
71
|
+
for (const [el, row, at] of rows) {
|
|
72
|
+
const p = up(row, p => n.get(p)! > 1) || row.parentElement
|
|
73
|
+
by.has(p) || by.set(p, [])
|
|
74
|
+
by.get(p)!.push([isNaN(at) ? by.get(p)!.length : at, String(d(el))])
|
|
75
|
+
}
|
|
76
|
+
return [...by.values()].map(g => g.sort((x, y) => x[0] - y[0]).map(x => x[1]))
|
|
77
|
+
} catch (_) { return }
|
|
78
|
+
}
|
|
79
|
+
// (shown groups, host state, from keys, idField, uses key) at a keyboard pick-up or a press
|
|
80
|
+
const groups: string[][] = Array.isArray(a?.[0]) ? a : [a || []]
|
|
81
|
+
for (const l of c || []) {
|
|
82
|
+
if (onced(`SYG435:${e}:${l}`)) continue
|
|
83
|
+
const at = new Map<string, number>()
|
|
84
|
+
if (Array.isArray(b?.[l])) b[l].forEach((x: any, i: number) => at.set(String(x?.[d]), i))
|
|
85
|
+
// the list's container: the one holding most of its entries (others: stray matches)
|
|
86
|
+
let dom: number[] = []
|
|
87
|
+
for (const g of groups) {
|
|
88
|
+
const ix = g.filter(i => at.has(i)).map(i => at.get(i)!)
|
|
89
|
+
if (ix.length > dom.length) dom = ix
|
|
90
|
+
}
|
|
91
|
+
// a contiguous run in the array's order is fine (all of it, or a window of it)
|
|
92
|
+
if (!dom.some((v, j) => v !== dom[0] + j)) continue
|
|
93
|
+
if (once(`SYG435:${e}:${l}`)) devReport('SYG435', {
|
|
94
|
+
component: 'sortable',
|
|
95
|
+
message: `sortable '${e}': state.${l} is shown ${dom.length < at.size ? 'with entries hidden between the shown ones' : 'in another order'} (a Collection with ${dom.length < at.size ? 'filter' : 'sort'}?), but moves and the announced positions follow the array: a keyboard step can pass entries nobody sees, or change nothing visible`,
|
|
96
|
+
fix: `Reorder a list that is shown as it is stored: drop the Collection's sort (sort state.${l} itself), and hide the drag handles while a filter hides entries`,
|
|
97
|
+
data: {from: l, key: e},
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (code == 147) {
|
|
103
|
+
const st = b || {}, arrays = Object.keys(st).filter(k => Array.isArray(st[k]))
|
|
104
|
+
const near = suggest(a, arrays)
|
|
105
|
+
if (once(`SYG147:${c}:${a}`)) devReport('SYG147', {
|
|
106
|
+
component: 'sortable',
|
|
107
|
+
message: `sortable '${c}': from names '${a}', but state.${a} is ${st[a] === undefined ? 'missing' : 'not an array'} when the host starts${near ? ` (did you mean '${near}'?)` : ''}; nothing in it can be moved`,
|
|
108
|
+
fix: `Name the host state's list: sortable({ from: '${near || arrays[0] || 'items'}', ... })${arrays.length ? ` (arrays in the state: ${arrays.join(', ')})` : `, and start it as an array (initialState: { ${a}: [] })`}`,
|
|
109
|
+
data: {from: a, key: c},
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const report146 = (option: string, selector: string, why: string, fix: string) => {
|
|
115
|
+
if (once(`SYG146:${option}:${selector}`)) devReport('SYG146', {
|
|
116
|
+
component: 'sortable',
|
|
117
|
+
message: `sortable: ${option} '${selector}' matches nothing: ${why}, so no item can be dragged`,
|
|
118
|
+
fix,
|
|
119
|
+
data: {option, selector},
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Install the `sortable` hook on the core bridge (sortable.ts calls it). */
|
|
124
|
+
export function installSortableHooks(): () => void {
|
|
125
|
+
const c = (globalThis as any).__SYGNAL_DIAGNOSTICS__
|
|
126
|
+
if (!c) return () => {}
|
|
127
|
+
c.sortable = reportSortable
|
|
128
|
+
return () => { if (c.sortable === reportSortable) c.sortable = undefined }
|
|
129
|
+
}
|
|
@@ -1,37 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SYG201 — STATE reducer dropped keys that existed in the previous state (warn)
|
|
3
3
|
* SYG202 — STATE reducer returned undefined (warn)
|
|
4
|
+
* SYG221 — set() called with a string (error, G-143): the string is spread
|
|
5
|
+
* into the state as keys '0', '1', ... (one per character)
|
|
6
|
+
* SYG238 — G-393: a root with a model but no initialState (and no state from renderComponent's
|
|
7
|
+
* `initialState` or an HMR swap): its state is undefined, so it renders nothing until an
|
|
8
|
+
* action sets one (the 5.x / PLAN-4 behaviour; a root without a model renders from
|
|
9
|
+
* `initialState || true`, G-172). onModel, at construction, once per component name.
|
|
10
|
+
* SYG222 — STATE reducer returned the object it got after mutating it in place (warn, dev;
|
|
11
|
+
* PLAN-4 GS-4): since 6.0 the same object back means "no change", so the mutation
|
|
12
|
+
* is ignored. Mechanism: onIntent (called before the model is read) wraps the
|
|
13
|
+
* instance's STATE reducers; the wrapper takes a shallow snapshot (keys and top-level
|
|
14
|
+
* values) before the reducer runs and compares it when the reducer returns the same
|
|
15
|
+
* reference; a behavior slice (state[key] of a `uses` entry) is snapshotted the same
|
|
16
|
+
* way (keys reported as 'key.field', G-214). Nested mutations (state.list.push) are
|
|
17
|
+
* below the snapshot. Zero bytes
|
|
18
|
+
* in the core: the wrapping only happens with the dev entry and diagnostics on.
|
|
4
19
|
*
|
|
5
20
|
* Mechanism: onReducer(component, action, prevState, nextState). Reported once
|
|
6
21
|
* per action per component name. Not reported for:
|
|
7
|
-
* - synthetic actions (`__*`) and INITIALIZE
|
|
22
|
+
* - synthetic actions (`__*`) and INITIALIZE (whole-state replacement; HYDRATE is an
|
|
23
|
+
* ordinary action since 6.0, D66)
|
|
24
|
+
* - SYG202: a Collection item (`() => undefined` is its documented self-removal, G-357)
|
|
8
25
|
* - keys that are calculated fields (they are re-derived after the reducer)
|
|
9
26
|
* - keys whose previous value was undefined
|
|
10
27
|
* The set()/toggle() helpers always spread the previous state, so they can't
|
|
11
28
|
* drop keys and never trigger SYG201.
|
|
12
29
|
*/
|
|
13
30
|
import type {DiagnosticCheck} from '../index'
|
|
14
|
-
import {report, once, nameOf, isPlainObject} from './shared'
|
|
31
|
+
import {report, devReport, once, nameOf, isPlainObject} from './shared'
|
|
32
|
+
import {ORIGINAL} from '../../../shared'
|
|
15
33
|
|
|
16
|
-
const SKIP = new Set(['INITIALIZE'
|
|
34
|
+
const SKIP = new Set(['INITIALIZE'])
|
|
35
|
+
|
|
36
|
+
// SYG222: the instance's STATE reducers, wrapped (see the header)
|
|
37
|
+
const WRAPPED = Symbol('SYG222')
|
|
38
|
+
|
|
39
|
+
export function watchMutation(component: any, action: string, fn: any): any {
|
|
40
|
+
if (typeof fn !== 'function' || fn[WRAPPED]) return fn
|
|
41
|
+
const wrapped: any = (state: any, ...rest: any[]) => {
|
|
42
|
+
if (!isPlainObject(state)) return fn(state, ...rest)
|
|
43
|
+
// the state's top level, and (G-214) each behavior slice's: a behavior reducer that mutates
|
|
44
|
+
// its slice gets the host state back unchanged (behaviors.ts), so state[key] looks the same
|
|
45
|
+
const snaps = [[state, ''], ...(component._uses || []).map(([k]: any) => [state[k], k + '.'])]
|
|
46
|
+
.filter(([o]) => isPlainObject(o)).map(([o, p]) => [o, p, Object.keys(o), Object.values(o)])
|
|
47
|
+
const out = fn(state, ...rest)
|
|
48
|
+
if (out === state) {
|
|
49
|
+
const changed = snaps.flatMap(([o, p, keys, values]: any) => keys.filter((k: string, i: number) => !(k in o) || o[k] !== values[i])
|
|
50
|
+
.concat(Object.keys(o).filter(k => !keys.includes(k))).map((k: string) => p + k))
|
|
51
|
+
const name = nameOf(component)
|
|
52
|
+
if (changed.length && once(`SYG222:${name}:${action}`)) {
|
|
53
|
+
const list = changed.map(k => `'${k}'`).join(', ')
|
|
54
|
+
devReport('SYG222', {
|
|
55
|
+
component,
|
|
56
|
+
message: `The STATE reducer for '${action}' changed ${list} in place and returned the same object. Returning the object a reducer got means "no change" (as ABORT), so the change is ignored and nothing re-renders`,
|
|
57
|
+
fix: `Return a new object: ${changed[0].includes('.') ? `(slice, data) => ({ ...slice, ${changed[0].split('.')[1]}: … })` : `(state, data) => ({ ...state, ${changed[0]}: … })`}. To write updates as mutations, wrap the reducer in immer's produce()`,
|
|
58
|
+
data: {action, keys: changed},
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
wrapped[WRAPPED] = true
|
|
65
|
+
// checks that read reducer source (reply actions) see the original; so do the action log
|
|
66
|
+
// and t.explain() (ORIGINAL)
|
|
67
|
+
wrapped.toString = () => fn.toString()
|
|
68
|
+
wrapped[ORIGINAL] = fn
|
|
69
|
+
return wrapped
|
|
70
|
+
}
|
|
17
71
|
|
|
18
72
|
export const stateCheck: DiagnosticCheck = {
|
|
19
73
|
id: 'state',
|
|
20
74
|
|
|
75
|
+
// SYG222: checks/next.ts wraps the STATE reducers (watchMutation) through wrapHandler
|
|
76
|
+
|
|
77
|
+
onModel(component) {
|
|
78
|
+
// SYG238 (G-393): the initial state is set at construction (D165), before onModel
|
|
79
|
+
const n = component?.__next
|
|
80
|
+
if (!n?.isRoot || !n.def?.model || n.state !== undefined) return
|
|
81
|
+
const name = nameOf(component)
|
|
82
|
+
if (!once(`SYG238:${name}`)) return
|
|
83
|
+
devReport('SYG238', {
|
|
84
|
+
component,
|
|
85
|
+
message: `${name} is a root with a model but no initialState: its state is undefined, so it renders nothing until an action sets one`,
|
|
86
|
+
fix: `Add ${name}.initialState = { … } (the root's start state; a child gets its state from its parent instead)`,
|
|
87
|
+
data: {},
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
|
|
21
91
|
onReducer(component, action, prevState, nextState) {
|
|
22
92
|
if (typeof action !== 'string' || action.startsWith('__') || SKIP.has(action)) return
|
|
23
93
|
const name = nameOf(component)
|
|
24
94
|
|
|
25
95
|
if (nextState === undefined) {
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
//
|
|
96
|
+
// G-357: returning undefined from a Collection item's reducer is the documented way to
|
|
97
|
+
// remove the item: not reported. In another sub-component it is info, in the root (where
|
|
98
|
+
// it wipes the whole app state) a warning.
|
|
99
|
+
if (component?.__next?.kind === 'item' || !once(`SYG202:${name}:${action}`)) return
|
|
29
100
|
const sub = !!component?.isSubComponent
|
|
30
101
|
report('SYG202', {
|
|
31
102
|
component,
|
|
32
103
|
severity: sub ? 'info' : 'warn',
|
|
33
104
|
message: `The STATE reducer for '${action}' returned undefined` +
|
|
34
|
-
(sub ? ' (
|
|
105
|
+
(sub ? ' (only a Collection item removes itself this way)' : ''),
|
|
35
106
|
fix: `Return the new state, e.g. (state, data) => ({ ...state, ... }), or return ABORT to leave the state unchanged`,
|
|
36
107
|
data: {action},
|
|
37
108
|
})
|
|
@@ -39,6 +110,28 @@ export const stateCheck: DiagnosticCheck = {
|
|
|
39
110
|
}
|
|
40
111
|
|
|
41
112
|
if (!isPlainObject(prevState) || !isPlainObject(nextState)) return
|
|
113
|
+
|
|
114
|
+
// SYG221 (G-143): set('city') spreads the string into the state: { ...state, 0: 'c', 1: 'i', ... }
|
|
115
|
+
const chars: string[] = []
|
|
116
|
+
while (typeof nextState[chars.length] === 'string' && nextState[chars.length].length === 1 && !(chars.length in prevState)) {
|
|
117
|
+
chars.push(nextState[chars.length])
|
|
118
|
+
}
|
|
119
|
+
if (chars.length > 0 && !(chars.length in nextState)) {
|
|
120
|
+
if (!once(`SYG221:${name}:${action}`)) return
|
|
121
|
+
const field = chars.join('')
|
|
122
|
+
const update = /^[A-Za-z_$][\w$]*$/.test(field)
|
|
123
|
+
? `set((state, ${field}) => ({ ${field} }))`
|
|
124
|
+
: `set((state, value) => ({ ['${field}']: value }))`
|
|
125
|
+
devReport('SYG221', {
|
|
126
|
+
component,
|
|
127
|
+
message: `The STATE reducer for '${action}' added the keys ${chars.map((_, i) => `'${i}'`).slice(0, 4).join(', ')}${chars.length > 4 ? ', …' : ''} ` +
|
|
128
|
+
`(one per character of '${field}'): set() was called with a string, set('${field}'). set() takes an object or a function`,
|
|
129
|
+
fix: `To store the action data in '${field}', pass a function: ${update}. For a fixed value pass an object: set({ ${field}: … })`,
|
|
130
|
+
data: {action, field},
|
|
131
|
+
})
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
42
135
|
const calculated: Set<string> | null = component?._calculatedFieldNames || null
|
|
43
136
|
const dropped = Object.keys(prevState).filter(key =>
|
|
44
137
|
!(key in nextState) && prevState[key] !== undefined && !(calculated && calculated.has(key)))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-4.5 D152: shared statics are frozen in dev (dev entry only; the core never freezes).
|
|
3
|
+
*
|
|
4
|
+
* Since P45-B a component's `initialState`, `model`, `context` and `calculated` statics are
|
|
5
|
+
* shared by all its instances (no per-instance deep copy). Mutating one in place (a reducer's
|
|
6
|
+
* `state.items.push(x)` on the initial state, a view that writes into state) would change every
|
|
7
|
+
* instance, and every later one. With diagnostics on, `initialState` is deep-frozen (its plain
|
|
8
|
+
* objects and arrays; class instances such as Map or Date are left as they are) and the other
|
|
9
|
+
* statics are frozen at the top level, so such a mutation throws a TypeError where it happens
|
|
10
|
+
* (in a reducer: reported as SYG216 with the error attached; in a view: SYG406).
|
|
11
|
+
*
|
|
12
|
+
* Frozen: the component function's own statics, and the options given to component() (G-280:
|
|
13
|
+
* `component({ view, model, initialState })`; the instance's `_o`, the values before behaviors or
|
|
14
|
+
* persist() made per-instance copies). Not frozen: the instance's values (G-268), and an
|
|
15
|
+
* initial state a wrapper built from its caller's objects (G-275: marked with `owned()` by
|
|
16
|
+
* sygnal/element (host props), Vike (pageContext.data, hydrated state) and renderComponent's
|
|
17
|
+
* `initialState` option; the caller may still change those). Mechanism: onIntent, which runs
|
|
18
|
+
* during construction, before the model is wired.
|
|
19
|
+
*/
|
|
20
|
+
import type {DiagnosticCheck} from '../index'
|
|
21
|
+
import {OWNED} from '../../owned'
|
|
22
|
+
|
|
23
|
+
const plain = (v: any): boolean => {
|
|
24
|
+
if (!v || typeof v != 'object') return false
|
|
25
|
+
const p = Object.getPrototypeOf(v)
|
|
26
|
+
return Array.isArray(v) || p === Object.prototype || p === null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const deepFreeze = (v: any): void => {
|
|
30
|
+
if (!plain(v) || Object.isFrozen(v) || v[OWNED]) return
|
|
31
|
+
Object.freeze(v)
|
|
32
|
+
for (const k of Object.keys(v)) deepFreeze(v[k])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const freeze = (o: any): void => {
|
|
36
|
+
if (!o || (typeof o != 'object' && typeof o != 'function')) return
|
|
37
|
+
deepFreeze(o.initialState)
|
|
38
|
+
for (const k of ['model', 'context', 'calculated']) if (plain(o[k])) Object.freeze(o[k])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const staticsCheck: DiagnosticCheck = {
|
|
42
|
+
id: 'statics',
|
|
43
|
+
|
|
44
|
+
onIntent(component) {
|
|
45
|
+
if (typeof component?.view == 'function') freeze(component.view)
|
|
46
|
+
freeze(component?._o)
|
|
47
|
+
},
|
|
48
|
+
}
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
*
|
|
14
14
|
* | Code | Rule (canonical form) | Runtime mechanism |
|
|
15
15
|
* |--------|------------------------------------------|-----------------------------------------|
|
|
16
|
-
* |
|
|
17
|
-
* |
|
|
18
|
-
* | | |
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* | SYG508 | reply actions, not select()/errors() | replies.ts: a request with a category |
|
|
17
|
+
* | | round trip (PLAN-3 §1.1) | the same instance select()ed on a |
|
|
18
|
+
* | | | reply-capable source |
|
|
19
|
+
*
|
|
20
|
+
* SYG501 (positional views) and SYG504 ('ACTION | SINK' keys) are forms Sygnal 6.0 removed
|
|
21
|
+
* (D164): at run time they are SYG612 (checks/next.ts, strict or not); `sygnal-check --strict`
|
|
22
|
+
* still reports them under their own codes, with --fix. SYG502 is retired (PLAN-4 GS-4). SYG503,
|
|
23
|
+
* SYG505, SYG506, SYG507: static only (sygnal-check --strict).
|
|
23
24
|
*
|
|
24
25
|
* Every finding is reported once per component name (+ action / key).
|
|
25
26
|
* The SYG5xx severities are registered here (registerCodes) and passed
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
import type {DiagnosticCheck} from '../index'
|
|
29
30
|
import {CODE_TITLES, STRICT_CODE_SEVERITY, registerCodes} from '../codes'
|
|
30
31
|
import type {DiagnosticSeverity} from '../codes'
|
|
31
|
-
import {bridge
|
|
32
|
+
import {bridge} from './shared'
|
|
32
33
|
|
|
33
34
|
registerCodes(Object.keys(STRICT_CODE_SEVERITY).map(code =>
|
|
34
35
|
[code, STRICT_CODE_SEVERITY[code], CODE_TITLES[code]] as [string, DiagnosticSeverity, string]))
|
|
@@ -48,65 +49,7 @@ export function isStrictEnabled(): boolean {
|
|
|
48
49
|
return flag === undefined ? (globalThis as any).__SYGNAL_STRICT__ === true : flag === true
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
report(code, {severity: STRICT_CODE_SEVERITY[code], ...details})
|
|
53
|
-
|
|
52
|
+
/** The strict check's runtime half is SYG508 (replies.ts); this entry keeps the flag. */
|
|
54
53
|
export const strictCheck: DiagnosticCheck = {
|
|
55
54
|
id: 'strict',
|
|
56
|
-
|
|
57
|
-
onModel(component) {
|
|
58
|
-
if (!isStrictEnabled() || !component) return
|
|
59
|
-
const name = nameOf(component)
|
|
60
|
-
|
|
61
|
-
// SYG501 — view(props, state, context, peers): positional use of the 2nd+ args.
|
|
62
|
-
// (A default value or a rest parameter stops Function.length, so this can
|
|
63
|
-
// miss `(props, state = {})`, but it never flags a single-argument view.)
|
|
64
|
-
const view = component.view
|
|
65
|
-
if (typeof view === 'function' && view.length > 1 && !view.__sygnalLazy && once(`SYG501:${name}`)) {
|
|
66
|
-
const extra = ['state', 'context', 'peers'].slice(0, view.length - 1)
|
|
67
|
-
strictReport('SYG501', {
|
|
68
|
-
component,
|
|
69
|
-
message: `The view takes ${view.length} positional arguments (props, ${extra.join(', ')})`,
|
|
70
|
-
fix: `Destructure the first argument instead: function ${name}({ ${extra.join(', ')}, ...props })`,
|
|
71
|
-
data: {arity: view.length},
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// SYG504 — 'ACTION | SINK' shorthand keys
|
|
76
|
-
const model = component.model
|
|
77
|
-
if (!model || typeof model !== 'object') return
|
|
78
|
-
for (const key of Object.keys(model)) {
|
|
79
|
-
if (!key.includes('|')) continue
|
|
80
|
-
const [action, sink] = key.split('|').map(s => s.trim())
|
|
81
|
-
if (!action || !sink || !once(`SYG504:${name}:${key}`)) continue
|
|
82
|
-
const rewrite = sink === 'EVENTS'
|
|
83
|
-
? `${action}: { EVENTS: event('TYPE', (state, data) => payload) }`
|
|
84
|
-
: `${action}: { ${sink}: (state, data, next) => ... }`
|
|
85
|
-
strictReport('SYG504', {
|
|
86
|
-
component,
|
|
87
|
-
message: `Model key '${key}' uses the 'ACTION | SINK' shorthand`,
|
|
88
|
-
fix: `Use the object form: ${rewrite} (merge it into an existing '${action}' entry if there is one)`,
|
|
89
|
-
data: {key, action, sink},
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
onReducer(component, action, prevState, nextState) {
|
|
95
|
-
if (!isStrictEnabled() || isInternalAction(action)) return // incl. INITIALIZE/HYDRATE/DISPOSE
|
|
96
|
-
// undefined is SYG202 (state check, every mode); only the identical object here.
|
|
97
|
-
if (!prevState || typeof prevState !== 'object' || nextState === undefined) return
|
|
98
|
-
let same = nextState === prevState
|
|
99
|
-
if (!same && typeof component?.addCalculated === 'function') {
|
|
100
|
-
// with calculated fields the reducer saw addCalculated(prev) (memoized: same object)
|
|
101
|
-
try { same = component.addCalculated(prevState) === nextState } catch (_) {}
|
|
102
|
-
}
|
|
103
|
-
const name = nameOf(component)
|
|
104
|
-
if (!same || !once(`SYG502:${name}:${action}`)) return
|
|
105
|
-
strictReport('SYG502', {
|
|
106
|
-
component,
|
|
107
|
-
message: `The STATE reducer for '${action}' returned the unchanged state object`,
|
|
108
|
-
fix: `Return ABORT for "no change": ${action}: (state, data) => cond ? { ...state, ... } : ABORT`,
|
|
109
|
-
data: {action},
|
|
110
|
-
})
|
|
111
|
-
},
|
|
112
55
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timers and declaration statics without a driver (PLAN-4 GS-7). Dev-entry-only codes
|
|
3
|
+
* (DEV_CODE_SEVERITY):
|
|
4
|
+
*
|
|
5
|
+
* SYG643 — a component declares a static that only a registered driver acts on (`timers`:
|
|
6
|
+
* makeTimerDriver(), `connections`: makeSocketDriver(), `resources`: makeFetchDriver(),
|
|
7
|
+
* `browser`: makeBrowserDriver())
|
|
8
|
+
* and no source of the app takes it (no `__sygnalStatic` marker for it), so nothing
|
|
9
|
+
* happens and nothing else says so. Also closes PLAN-3's "connections without a driver
|
|
10
|
+
* report nothing" gap. (`route` and `head` are left out: renderComponent asks for the
|
|
11
|
+
* router itself, and `head` is also collected by renderToString without a driver.)
|
|
12
|
+
* SYG422 — a timer spec makeTimerDriver() can't run (non-positive `every`, negative `after`,
|
|
13
|
+
* `every` with `after`, no action, `frame` that isn't an action name): it isn't started.
|
|
14
|
+
* The driver calls `__SYGNAL_DIAGNOSTICS__.timerSpec(name, spec, componentName)`, which
|
|
15
|
+
* installTimerHooks() sets.
|
|
16
|
+
*/
|
|
17
|
+
import type {DiagnosticCheck} from '../index'
|
|
18
|
+
import {bridge, devReport, once, nameOf} from './shared'
|
|
19
|
+
|
|
20
|
+
const NEEDS: Record<string, string> = {
|
|
21
|
+
timers: 'makeTimerDriver()',
|
|
22
|
+
connections: 'makeSocketDriver()',
|
|
23
|
+
resources: 'makeFetchDriver()',
|
|
24
|
+
browser: 'makeBrowserDriver()',
|
|
25
|
+
}
|
|
26
|
+
const KEY: Record<string, string> = {timers: 'TIMER', connections: 'WS', resources: 'HTTP', browser: 'BROWSER'}
|
|
27
|
+
|
|
28
|
+
const brief = (v: any) => { try { return JSON.stringify(v) } catch (_) { return String(v) } }
|
|
29
|
+
|
|
30
|
+
/** why a timer spec can't run ('' when it can; mirrors timers.ts's valid()) */
|
|
31
|
+
export function timerSpecProblem(s: any): string {
|
|
32
|
+
if (!s || typeof s != 'object') return 'is not an object ({ every, action }, { after, action } or { frame: action })'
|
|
33
|
+
if ('frame' in s && s.frame) return typeof s.frame == 'string' ? '' : 'frame must be the action name (a string)'
|
|
34
|
+
if (!s.action || typeof s.action != 'string') return 'has no action (a string)'
|
|
35
|
+
if (s.every == null && s.after == null) return 'has neither every nor after'
|
|
36
|
+
if (s.every != null && s.after != null) return 'has both every and after'
|
|
37
|
+
if (s.every != null) return s.every > 0 && s.every < 1 / 0 ? '' : `every must be a positive number of ms (got ${brief(s.every)})`
|
|
38
|
+
return s.after >= 0 && s.after < 1 / 0 ? '' : `after must be a number of ms, 0 or more (got ${brief(s.after)})`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function checkSpec(name: string, spec: any, component?: string) {
|
|
42
|
+
const problem = timerSpecProblem(spec) || 'is invalid'
|
|
43
|
+
if (!once(`SYG422:${component}:${name}:${brief(spec)}`)) return
|
|
44
|
+
devReport('SYG422', {
|
|
45
|
+
component,
|
|
46
|
+
message: `Timer '${name}' ${problem}; it is not started`,
|
|
47
|
+
fix: `Declare ${name}: { every: 100, action: 'TICK' }, { after: 5000, action: 'EXPIRE' } or { frame: 'FRAME' } (falsy to stop it)`,
|
|
48
|
+
data: {name, spec},
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** install the hook makeTimerDriver() calls for an invalid spec (removed by the returned function) */
|
|
53
|
+
export function installTimerHooks(): () => void {
|
|
54
|
+
const core = bridge()
|
|
55
|
+
if (!core) return () => {}
|
|
56
|
+
core.timerSpec = checkSpec
|
|
57
|
+
return () => { if (core.timerSpec === checkSpec) core.timerSpec = undefined }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const timersCheck: DiagnosticCheck = {
|
|
61
|
+
id: 'timers',
|
|
62
|
+
|
|
63
|
+
onModel(component) {
|
|
64
|
+
const sources = component?.sources, view = component?.view
|
|
65
|
+
if (!sources || typeof sources != 'object' || !view) return
|
|
66
|
+
const names = component.sourceNames || Object.keys(sources)
|
|
67
|
+
for (const k in NEEDS) {
|
|
68
|
+
if (view[k] == null) continue
|
|
69
|
+
if (names.some((n: string) => sources[n]?.__sygnalStatic === k)) continue
|
|
70
|
+
const name = nameOf(component)
|
|
71
|
+
if (!once(`SYG643:${name}:${k}`)) continue
|
|
72
|
+
devReport('SYG643', {
|
|
73
|
+
component,
|
|
74
|
+
message: `${name} declares ${name}.${k}, but no ${NEEDS[k]} driver is registered, so ${k == 'timers' ? 'its timers never run' : k == 'connections' ? 'its connections never open' : k == 'browser' ? 'its browser sources never start' : 'its resources are never fetched'}`,
|
|
75
|
+
fix: `Register the driver: run(App, { ${KEY[k]}: ${NEEDS[k]} })`,
|
|
76
|
+
data: {static: k, driver: NEEDS[k]},
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View Transitions (PLAN-4 GS-12, D129). Dev-entry-only code (DEV_CODE_SEVERITY):
|
|
3
|
+
*
|
|
4
|
+
* SYG645 — a component declares `viewTransitions`, but its app's DOM driver can't run them (run()'s
|
|
5
|
+
* default makeDOMDriver, not makeViewTransitionDOMDriver()), so its actions patch at once
|
|
6
|
+
* and nothing else says so. The helper marks its IsolateModule (`vtDriver`). Not reported
|
|
7
|
+
* for renderComponent's mock DOM (no IsolateModule) or its `dom: 'real'` container
|
|
8
|
+
* (`.sygnal-test`): tests don't animate.
|
|
9
|
+
* Also (G-228): the static is not an array (`true`, `'MOVE'`): no action is listed (the core
|
|
10
|
+
* reads it with `includes?.()`, so a string matches substrings; anything else matches none).
|
|
11
|
+
*/
|
|
12
|
+
import type {DiagnosticCheck} from '../index'
|
|
13
|
+
import {devReport, once, nameOf} from './shared'
|
|
14
|
+
|
|
15
|
+
export const viewTransitionsCheck: DiagnosticCheck = {
|
|
16
|
+
id: 'viewTransitions',
|
|
17
|
+
|
|
18
|
+
onModel(component) {
|
|
19
|
+
const list = component?.view?.viewTransitions
|
|
20
|
+
if (list == null) return
|
|
21
|
+
if (!Array.isArray(list)) {
|
|
22
|
+
const name = nameOf(component)
|
|
23
|
+
if (!once(`SYG645:${name}`)) return
|
|
24
|
+
const actions = Object.keys(component.model || {}).filter(a => !a.includes('|'))
|
|
25
|
+
devReport('SYG645', {
|
|
26
|
+
component,
|
|
27
|
+
message: `${name}.viewTransitions is not an array (it is ${typeof list == 'string' ? `'${list}'` : typeof list}), so no action is listed for a View Transition`,
|
|
28
|
+
fix: `List the action names in an array: ${name}.viewTransitions = [${(typeof list == 'string' ? [list] : actions.slice(0, 2)).map(a => `'${a}'`).join(', ') || "'ACTION'"}] (an array of action names)`,
|
|
29
|
+
data: {value: list},
|
|
30
|
+
})
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if (!list.length) return
|
|
34
|
+
const im = component.sources?.[component.DOMSourceName || 'DOM']?._isolateModule
|
|
35
|
+
// the core's DOM source proxy answers any unknown key with an event shorthand (a function)
|
|
36
|
+
if (typeof im != 'object' || !im || im.vtDriver) return
|
|
37
|
+
if (typeof document != 'undefined' && document.querySelector?.('.sygnal-test')) return
|
|
38
|
+
const name = nameOf(component)
|
|
39
|
+
if (!once(`SYG645:${name}`)) return
|
|
40
|
+
devReport('SYG645', {
|
|
41
|
+
component,
|
|
42
|
+
message: `${name} declares ${name}.viewTransitions (${list.map((a: any) => `'${a}'`).join(', ')}), but the app's DOM driver can't run View Transitions, so those actions patch without one`,
|
|
43
|
+
fix: `Give run() the View Transition DOM driver: run(App, { DOM: makeViewTransitionDOMDriver('#root') })`,
|
|
44
|
+
data: {actions: list},
|
|
45
|
+
})
|
|
46
|
+
},
|
|
47
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-5 V-1: `<VirtualCollection>` diagnostics (dev entry; src/extra/virtual.ts carries no text).
|
|
3
|
+
*
|
|
4
|
+
* The VirtualCollection host calls `__SYGNAL_DIAGNOSTICS__.virtual(code, owner, extra)`:
|
|
5
|
+
* 430 the container has no bounded height: `extra.reason` 'height' (laid out 0 px tall) or
|
|
6
|
+
* 'grows' (as tall as all its rows and taller than the viewport, so it would render every
|
|
7
|
+
* row); the window is clamped to the viewport (once per host and reason)
|
|
8
|
+
* 431 items without `id` (index keys; once per host)
|
|
9
|
+
* 432 an item renders a fragment or text, not one element (once per host)
|
|
10
|
+
* 433 scrollToIndex / scrollToId for an index or id not in the (filtered) list
|
|
11
|
+
* 434 estimateSize / overscan not a positive number (or a function, for estimateSize)
|
|
12
|
+
*/
|
|
13
|
+
import {devReport, nameOf} from './shared'
|
|
14
|
+
|
|
15
|
+
const tag = (owner: any) => `<VirtualCollection> in ${nameOf(owner)}`
|
|
16
|
+
|
|
17
|
+
export function reportVirtual(code: number, owner: any, x: any = {}): any {
|
|
18
|
+
const component = owner ? nameOf(owner) : 'VirtualCollection'
|
|
19
|
+
if (code == 430) {
|
|
20
|
+
const grows = x.reason == 'grows'
|
|
21
|
+
return devReport('SYG430', {
|
|
22
|
+
component,
|
|
23
|
+
message: grows
|
|
24
|
+
? `${tag(owner)} grows with its rows (it is ${Math.round(x.height)} px tall, as tall as all of them), so it is not a scroll container; only the rows within the viewport's height render`
|
|
25
|
+
: `${tag(owner)} is 0 px tall, so no row is in view`,
|
|
26
|
+
fix: 'Give its className a bounded height (height or max-height, e.g. .rows { height: 400px }, or flex: 1 with min-height: 0 in a flex column); it scrolls itself (overflow-y: auto)',
|
|
27
|
+
data: x,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
if (code == 431) {
|
|
31
|
+
return devReport('SYG431', {
|
|
32
|
+
component,
|
|
33
|
+
message: `${tag(owner)} has items without an id (${x.count} items): they are keyed by their index, so a row's instance and measured height follow its position, not its item, when items are added, removed or reordered`,
|
|
34
|
+
fix: "Give every item a unique id (e.g. rows: data.map((r, i) => ({ id: r.key ?? i, ...r })))",
|
|
35
|
+
data: x,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
if (code == 432) {
|
|
39
|
+
return devReport('SYG432', {
|
|
40
|
+
component,
|
|
41
|
+
message: `${tag(owner)}: its item component ${x.component} renders a fragment or text, so its rows can't be measured, positioned or given aria-posinset`,
|
|
42
|
+
fix: `Make ${x.component}'s view return one element (wrap its content in a <div> or <li>)`,
|
|
43
|
+
data: x,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
if (code == 433) {
|
|
47
|
+
const what = 'id' in x ? `id ${JSON.stringify(x.id) ?? String(x.id)}` : `index ${x.index}`
|
|
48
|
+
return devReport('SYG433', {
|
|
49
|
+
component,
|
|
50
|
+
message: `${tag(owner)}: ${'id' in x ? 'scrollToId' : 'scrollToIndex'} for ${what}, which is not in its list (${x.count} rows after filter); it didn't scroll`,
|
|
51
|
+
fix: 'id' in x
|
|
52
|
+
? 'Send the id of an item in the array the filter keeps: { scrollToId: \'.rows\', id }'
|
|
53
|
+
: `Send an integer index from 0 to ${Math.max(0, x.count - 1)} into the shown (filtered, sorted) rows: { scrollToIndex: '.rows', index }`,
|
|
54
|
+
data: x,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
if (code == 434) {
|
|
58
|
+
return devReport('SYG434', {
|
|
59
|
+
component,
|
|
60
|
+
message: `${tag(owner)}: ${x.prop}={${typeof x.value == 'string' ? JSON.stringify(x.value) : String(x.value)}} is not ${x.prop == 'overscan' ? 'a number ≥ 0; 5 is used' : 'a positive number or a function; 32 is used'}`,
|
|
61
|
+
fix: x.prop == 'overscan' ? 'Pass overscan={5} (rows rendered beyond each edge)' : 'Pass estimateSize={32} (about a row\'s height in px), or (item, index) => px',
|
|
62
|
+
data: x,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Publish the VirtualCollection hook on the core bridge. Returns an uninstall function. */
|
|
68
|
+
export function installVirtualHooks(): () => void {
|
|
69
|
+
const c = (globalThis as any).__SYGNAL_DIAGNOSTICS__
|
|
70
|
+
if (!c) return () => {}
|
|
71
|
+
c.virtual = reportVirtual
|
|
72
|
+
return () => { if (c.virtual === reportVirtual) c.virtual = undefined }
|
|
73
|
+
}
|