sygnal 5.3.7 → 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 +77 -47
- package/dist/astro/client.cjs.js +33 -8114
- package/dist/astro/client.cjs.js.map +1 -0
- package/dist/astro/client.mjs +33 -8114
- package/dist/astro/client.mjs.map +1 -0
- package/dist/astro/index.cjs.js +1039 -9
- package/dist/astro/index.cjs.js.map +1 -0
- package/dist/astro/index.mjs +1038 -9
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/server.cjs.js +3400 -166
- package/dist/astro/server.cjs.js.map +1 -0
- package/dist/astro/server.mjs +3400 -166
- package/dist/astro/server.mjs.map +1 -0
- 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 +4033 -0
- package/dist/diagnostics.cjs.js.map +1 -0
- package/dist/diagnostics.esm.js +4003 -0
- package/dist/diagnostics.esm.js.map +1 -0
- 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 +15283 -6279
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +3739 -370
- package/dist/index.esm.js +15230 -6274
- package/dist/index.esm.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs.js +27 -329
- package/dist/jsx-dev-runtime.cjs.js.map +1 -0
- package/dist/jsx-dev-runtime.esm.js +20 -322
- package/dist/jsx-dev-runtime.esm.js.map +1 -0
- package/dist/jsx-runtime.cjs.js +27 -329
- package/dist/jsx-runtime.cjs.js.map +1 -0
- package/dist/jsx-runtime.esm.js +20 -322
- package/dist/jsx-runtime.esm.js.map +1 -0
- package/dist/jsx.cjs.js +11 -309
- package/dist/jsx.cjs.js.map +1 -0
- package/dist/jsx.esm.js +9 -310
- package/dist/jsx.esm.js.map +1 -0
- 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 +2 -1
- package/dist/sygnal.min.js.map +1 -0
- 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 +5 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -0
- package/dist/vike/ClientOnly.mjs +5 -14
- package/dist/vike/ClientOnly.mjs.map +1 -0
- package/dist/vike/{+config.js → config/+config.js} +10 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +166 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -0
- package/dist/vike/onRenderClient.mjs +166 -95
- package/dist/vike/onRenderClient.mjs.map +1 -0
- package/dist/vike/onRenderHtml.cjs.js +60 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -0
- package/dist/vike/onRenderHtml.mjs +61 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -0
- package/dist/vite/plugin.cjs.js +926 -65
- package/dist/vite/plugin.cjs.js.map +1 -0
- package/dist/vite/plugin.mjs +925 -65
- package/dist/vite/plugin.mjs.map +1 -0
- 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 +313 -0
- package/package.json +109 -16
- package/src/astro/client.ts +40 -18
- package/src/astro/index.d.ts +48 -1
- package/src/astro/index.ts +106 -9
- package/src/astro/server.ts +11 -3
- package/src/collection.ts +6 -90
- 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 +13 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +76 -0
- package/src/cycle/dom/controlledInputModule.ts +50 -0
- package/src/cycle/dom/enrichEventStream.ts +29 -45
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -57
- package/src/cycle/dom/mockDOMSource.ts +83 -9
- package/src/cycle/dom/modules.ts +21 -4
- 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 +20 -4
- 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 +109 -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 +67 -0
- 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 +250 -0
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +83 -0
- 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 +202 -0
- package/src/extra/diagnostics/checks/inspect.ts +427 -0
- 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 +56 -0
- package/src/extra/diagnostics/checks/public.d.ts +302 -0
- 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 +88 -0
- package/src/extra/diagnostics/checks/shared.ts +203 -0
- 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 +148 -0
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +55 -0
- 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 +121 -0
- package/src/extra/diagnostics/codes.ts +477 -0
- package/src/extra/diagnostics/index.ts +353 -0
- package/src/extra/diagnostics/legacy.ts +76 -0
- package/src/extra/driverFactories.ts +172 -60
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/eventDriver.ts +4 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/flatten.ts +75 -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 +13 -6
- 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 +94 -210
- 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 +3158 -190
- 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/extra/xstreamExtras.ts +269 -0
- package/src/index.d.ts +3012 -87
- package/src/index.ts +32 -10
- 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 +262 -134
- 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 -119
- 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 +27 -4
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/plugin.d.ts +114 -2
- package/src/vite/plugin.ts +920 -56
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/astro/astro/client.d.ts +0 -5
- package/dist/astro/astro/index.d.ts +0 -16
- package/dist/astro/astro/server.d.ts +0 -12
- package/dist/astro/client.d.ts +0 -5
- package/dist/astro/client.esm.js +0 -4665
- package/dist/astro/collection.d.ts +0 -12
- package/dist/astro/component.d.ts +0 -22
- package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/astro/cycle/dom/index.d.ts +0 -8
- package/dist/astro/cycle/dom/isolate.d.ts +0 -9
- package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/astro/cycle/dom/modules.d.ts +0 -5
- package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
- package/dist/astro/cycle/dom/thunk.d.ts +0 -11
- package/dist/astro/cycle/dom/utils.d.ts +0 -8
- package/dist/astro/cycle/isolate/index.d.ts +0 -42
- package/dist/astro/cycle/run/adapt.d.ts +0 -6
- package/dist/astro/cycle/run/index.d.ts +0 -36
- package/dist/astro/cycle/run/internals.d.ts +0 -8
- package/dist/astro/cycle/run/types.d.ts +0 -49
- package/dist/astro/cycle/state/Collection.d.ts +0 -29
- package/dist/astro/cycle/state/StateSource.d.ts +0 -20
- package/dist/astro/cycle/state/index.d.ts +0 -5
- package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
- package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
- package/dist/astro/cycle/state/types.d.ts +0 -18
- package/dist/astro/cycle/state/withState.d.ts +0 -17
- package/dist/astro/extra/classes.d.ts +0 -7
- package/dist/astro/extra/devtools.d.ts +0 -78
- package/dist/astro/extra/dragDriver.d.ts +0 -20
- package/dist/astro/extra/driverFactories.d.ts +0 -12
- package/dist/astro/extra/eventDriver.d.ts +0 -9
- package/dist/astro/extra/exactState.d.ts +0 -1
- package/dist/astro/extra/hmr.d.ts +0 -17
- package/dist/astro/extra/logDriver.d.ts +0 -2
- package/dist/astro/extra/processDrag.d.ts +0 -19
- package/dist/astro/extra/processForm.d.ts +0 -15
- package/dist/astro/extra/run.d.ts +0 -13
- package/dist/astro/extra/xstreamCompat.d.ts +0 -5
- package/dist/astro/index.d.ts +0 -19
- package/dist/astro/index.esm.js +0 -27
- package/dist/astro/jsx-dev-runtime.d.ts +0 -1
- package/dist/astro/jsx-runtime.d.ts +0 -4
- package/dist/astro/jsx.d.ts +0 -2
- package/dist/astro/pragma/fn.d.ts +0 -7
- package/dist/astro/pragma/index.d.ts +0 -7
- package/dist/astro/pragma/is.d.ts +0 -10
- package/dist/astro/server.d.ts +0 -12
- package/dist/astro/server.esm.js +0 -25
- package/dist/astro/switchable.d.ts +0 -7
- package/dist/collection.d.ts +0 -12
- package/dist/component.d.ts +0 -22
- package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/cycle/dom/index.d.ts +0 -8
- package/dist/cycle/dom/isolate.d.ts +0 -9
- package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/cycle/dom/modules.d.ts +0 -5
- package/dist/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/cycle/dom/styleModule.d.ts +0 -13
- package/dist/cycle/dom/thunk.d.ts +0 -11
- package/dist/cycle/dom/utils.d.ts +0 -8
- package/dist/cycle/isolate/index.d.ts +0 -42
- package/dist/cycle/run/adapt.d.ts +0 -6
- package/dist/cycle/run/index.d.ts +0 -36
- package/dist/cycle/run/internals.d.ts +0 -8
- package/dist/cycle/run/types.d.ts +0 -49
- package/dist/cycle/state/Collection.d.ts +0 -29
- package/dist/cycle/state/StateSource.d.ts +0 -20
- package/dist/cycle/state/index.d.ts +0 -5
- package/dist/cycle/state/pickCombine.d.ts +0 -3
- package/dist/cycle/state/pickMerge.d.ts +0 -3
- package/dist/cycle/state/types.d.ts +0 -18
- package/dist/cycle/state/withState.d.ts +0 -17
- package/dist/extra/classes.d.ts +0 -7
- package/dist/extra/devtools.d.ts +0 -78
- package/dist/extra/dragDriver.d.ts +0 -20
- package/dist/extra/driverFactories.d.ts +0 -12
- package/dist/extra/eventDriver.d.ts +0 -9
- package/dist/extra/exactState.d.ts +0 -1
- package/dist/extra/hmr.d.ts +0 -17
- package/dist/extra/logDriver.d.ts +0 -2
- package/dist/extra/processDrag.d.ts +0 -19
- package/dist/extra/processForm.d.ts +0 -15
- package/dist/extra/run.d.ts +0 -13
- package/dist/extra/xstreamCompat.d.ts +0 -5
- package/dist/jsx-dev-runtime.d.ts +0 -1
- package/dist/jsx-dev-runtime.js +0 -224
- package/dist/jsx-runtime.d.ts +0 -4
- package/dist/jsx-runtime.js +0 -224
- package/dist/jsx.d.ts +0 -2
- package/dist/pragma/fn.d.ts +0 -7
- package/dist/pragma/index.d.ts +0 -7
- package/dist/pragma/is.d.ts +0 -10
- package/dist/switchable.d.ts +0 -7
- package/dist/vike/+config.cjs.js +0 -65
- package/src/component.ts +0 -2256
- 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 -174
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -167
- 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,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
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SYG105 — EVENTS type emitted but never selected, or selected but never
|
|
3
|
+
* emitted (info).
|
|
4
|
+
* SYG116 — a value with no string `type` (typically a function) reached the
|
|
5
|
+
* EVENTS bus (error, G-143).
|
|
6
|
+
*
|
|
7
|
+
* Mechanism: the EVENTS driver (src/extra/eventDriver.ts) calls onBusSelect
|
|
8
|
+
* for every EVENTS.select(type) and onBusEmit for every bus event.
|
|
9
|
+
* - emitted, nothing selects it: reported when the event is emitted. The bus
|
|
10
|
+
* is a broadcast, so an event nobody has subscribed to by then is dropped.
|
|
11
|
+
* - selected, never emitted: only knowable at the end of a session, so it is
|
|
12
|
+
* reported on demand by checkEventBus() (inspect / test helpers), never
|
|
13
|
+
* automatically.
|
|
14
|
+
* EVENTS.select() with no type (all events) counts as selecting everything.
|
|
15
|
+
*/
|
|
16
|
+
import type {DiagnosticCheck} from '../index'
|
|
17
|
+
import {report, devReport, once, onReset, didYouMean} from './shared'
|
|
18
|
+
|
|
19
|
+
let selected = new Set<string>()
|
|
20
|
+
let emitted = new Set<string>()
|
|
21
|
+
let selectsAll = false
|
|
22
|
+
|
|
23
|
+
onReset(() => { selected = new Set(); emitted = new Set(); selectsAll = false })
|
|
24
|
+
|
|
25
|
+
export const eventsCheck: DiagnosticCheck = {
|
|
26
|
+
id: 'events',
|
|
27
|
+
|
|
28
|
+
onBusSelect(type) {
|
|
29
|
+
if (type === undefined || type === null || type === '') { selectsAll = true; return }
|
|
30
|
+
for (const t of ([] as any[]).concat(type)) if (typeof t === 'string') selected.add(t)
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
onBusEmit(type, emitterName) {
|
|
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
|
+
}
|
|
48
|
+
emitted.add(type)
|
|
49
|
+
if (selectsAll || selected.has(type) || !once(`SYG105:emit:${type}`)) return
|
|
50
|
+
report('SYG105', {
|
|
51
|
+
component: emitterName,
|
|
52
|
+
message: `EVENTS event '${type}' was emitted, but nothing selects it, so it was dropped${didYouMean(type, selected)}`,
|
|
53
|
+
fix: `Subscribe with EVENTS.select('${type}') in the component that should react, or check the event name`,
|
|
54
|
+
data: {type, direction: 'emitted-not-selected', emitter: emitterName},
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface EventBusSummary {
|
|
60
|
+
selected: string[]
|
|
61
|
+
emitted: string[]
|
|
62
|
+
selectedNeverEmitted: string[]
|
|
63
|
+
emittedNeverSelected: string[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Report SYG105 (info) for every EVENTS type that has been selected but never
|
|
68
|
+
* emitted so far, and return the bus registry. Intended for inspect tooling and
|
|
69
|
+
* tests, at a point where the session is "done".
|
|
70
|
+
*/
|
|
71
|
+
export function checkEventBus(): EventBusSummary {
|
|
72
|
+
const selectedNeverEmitted = [...selected].filter(t => !emitted.has(t))
|
|
73
|
+
const emittedNeverSelected = selectsAll ? [] : [...emitted].filter(t => !selected.has(t))
|
|
74
|
+
for (const type of selectedNeverEmitted) {
|
|
75
|
+
if (!once(`SYG105:select:${type}`)) continue
|
|
76
|
+
report('SYG105', {
|
|
77
|
+
message: `EVENTS.select('${type}') is used, but no component has emitted '${type}'${didYouMean(type, emitted)}`,
|
|
78
|
+
fix: `Emit it from a model entry, e.g. { EVENTS: event('${type}', ...) }, or check the event name`,
|
|
79
|
+
data: {type, direction: 'selected-not-emitted'},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
return {selected: [...selected], emitted: [...emitted], selectedNeverEmitted, emittedNeverSelected}
|
|
83
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* makeFetchDriver requests (PLAN-3 5-3: the query cache, D79/D80; G-175). Dev-entry codes
|
|
3
|
+
* (DEV_CODE_SEVERITY):
|
|
4
|
+
*
|
|
5
|
+
* SYG630 — a cached request that is not idempotent: `cache: true` or `staleTime` on a
|
|
6
|
+
* POST/PUT/PATCH/DELETE (a request with a body defaults to POST) (warn)
|
|
7
|
+
* SYG631 — `validate` is not a Standard Schema (no `~standard.validate`): the request
|
|
8
|
+
* fails (error)
|
|
9
|
+
* SYG632 — `{ invalidate }` matched no cache entry and no mounted resource (info)
|
|
10
|
+
* SYG633 — `{ abort: 'X' }` cancels the lane 'X', but this instance sends its `ok: 'X'`
|
|
11
|
+
* requests under another `key`, so nothing is cancelled (warn)
|
|
12
|
+
* SYG635 — `cache: true` / `staleTime` / `{ prefetch }` sent to a driver without a
|
|
13
|
+
* `queryCache()` (D88): nothing is cached (warn). The source's `__inspect().cache`
|
|
14
|
+
* is undefined without one
|
|
15
|
+
*
|
|
16
|
+
* Mechanism: onModel wraps the component's own model stream for every sink whose source
|
|
17
|
+
* takes the `resources` static (makeFetchDriver and renderComponent's HTTP fake) and checks
|
|
18
|
+
* each value as it is sent: requests, and each entry of a `{ resources }` declaration.
|
|
19
|
+
* SYG632 asks the source how many entries/resources the value matches (`__matches`).
|
|
20
|
+
* Each is reported once per component name and request URL / name.
|
|
21
|
+
*/
|
|
22
|
+
import type {DiagnosticCheck} from '../index'
|
|
23
|
+
import {devReport, once, nameOf} from './shared'
|
|
24
|
+
import {isStandardSchema} from '../../standardSchema'
|
|
25
|
+
|
|
26
|
+
const IDEMPOTENT = /^(GET|HEAD)$/i
|
|
27
|
+
|
|
28
|
+
/** instance → reply key lanes it has sent, and ok name → the other key it was sent under */
|
|
29
|
+
const lanes = new WeakMap<object, {used: Set<string>; keyed: Map<string, string>}>()
|
|
30
|
+
|
|
31
|
+
const fetchSink = (component: any, name: string) => component?.sources?.[name]?.__sygnalStatic === 'resources'
|
|
32
|
+
/** 5-5: the sink's driver has no queryCache() */
|
|
33
|
+
const noCache = (component: any, sink: string) => {
|
|
34
|
+
const i = component?.sources?.[sink]?.__inspect?.()
|
|
35
|
+
return !!i && i.cache === undefined
|
|
36
|
+
}
|
|
37
|
+
const reportNoCache = (component: any, sink: string, what: string, url: string) => {
|
|
38
|
+
if (!noCache(component, sink) || !once(`SYG635:${nameOf(component)}:${what}:${url}`)) return
|
|
39
|
+
devReport('SYG635', {
|
|
40
|
+
component,
|
|
41
|
+
message: `${sink} ${url ? url + ' ' : ''}uses ${what}, but the ${sink} driver has no queryCache(), so nothing is cached`,
|
|
42
|
+
fix: `Give the driver a cache: makeFetchDriver({ cache: queryCache({ staleTime: 30000 }) }) (import { queryCache } from 'sygnal'); in tests renderComponent(C, { http: { cache: queryCache() } }). Or remove ${what}`,
|
|
43
|
+
data: {sink, url, what},
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkOne(component: any, sink: string, req: any, where: string) {
|
|
48
|
+
if (!req || typeof req !== 'object') return
|
|
49
|
+
const name = nameOf(component)
|
|
50
|
+
const url = typeof req.url === 'string' ? req.url : ''
|
|
51
|
+
const method = String(req.method || (req.json !== undefined || req.body !== undefined ? 'POST' : 'GET')).toUpperCase()
|
|
52
|
+
if ((req.cache === true || req.staleTime != null) && !IDEMPOTENT.test(method) && once(`SYG630:${name}:${method}:${url}`)) {
|
|
53
|
+
devReport('SYG630', {
|
|
54
|
+
component,
|
|
55
|
+
message: `${where} ${method} ${url} sets ${req.cache === true ? 'cache: true' : 'staleTime'}, so a cached reply answers it instead of sending it again`,
|
|
56
|
+
fix: `Cache reads only: remove ${req.cache === true ? 'cache' : 'staleTime'} from the ${method}, and refresh the cached reads after it succeeds with invalidates: ['tag']`,
|
|
57
|
+
data: {sink, method, url},
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
if (req.cache === true || req.staleTime != null) reportNoCache(component, sink, req.cache === true ? 'cache: true' : 'staleTime', url)
|
|
61
|
+
if ('validate' in req && !isStandardSchema(req.validate) && once(`SYG631:${name}:${url}`)) {
|
|
62
|
+
devReport('SYG631', {
|
|
63
|
+
component,
|
|
64
|
+
message: `${where} ${url} has validate: ${typeof req.validate}, which is not a Standard Schema, so the request fails`,
|
|
65
|
+
fix: "Pass a Standard Schema object (zod, valibot, arktype, ...: anything with a '~standard'.validate function), e.g. validate: QuoteSchema",
|
|
66
|
+
data: {sink, url},
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function check(component: any, sink: string, req: any) {
|
|
72
|
+
if (!req || typeof req !== 'object') return
|
|
73
|
+
const name = nameOf(component)
|
|
74
|
+
const res = req.resources
|
|
75
|
+
if (res && typeof res === 'object') {
|
|
76
|
+
for (const k of Object.keys(res)) checkOne(component, sink, typeof res[k] === 'string' ? {url: res[k]} : res[k], `${name}.resources.${k}:`)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
if (req.prefetch) {
|
|
80
|
+
const p = req.prefetch
|
|
81
|
+
reportNoCache(component, sink, '{ prefetch }', typeof p === 'string' ? p : String(p?.url ?? ''))
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
if ('invalidate' in req) {
|
|
85
|
+
const count = component.sources[sink]?.__matches?.(req.invalidate)
|
|
86
|
+
const what = typeof req.invalidate === 'function' ? 'the predicate' : JSON.stringify(req.invalidate)
|
|
87
|
+
if (count === 0 && once(`SYG632:${name}:${what}`)) {
|
|
88
|
+
devReport('SYG632', {
|
|
89
|
+
component,
|
|
90
|
+
message: `${sink} { invalidate: ${what} } matched no cache entry and no mounted resource`,
|
|
91
|
+
fix: "Give the resources (or cached requests) to refresh tags: ['quotes'] and invalidate that tag, or a URL prefix starting with '/' ('/api/quotes')",
|
|
92
|
+
data: {sink, invalidate: what},
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
let l = lanes.get(component)
|
|
98
|
+
if (!l) lanes.set(component, (l = {used: new Set(), keyed: new Map()}))
|
|
99
|
+
const {ok, error, key, abort} = req
|
|
100
|
+
if (typeof abort === 'string' && key === undefined) {
|
|
101
|
+
const other = l.keyed.get(abort)
|
|
102
|
+
if (other !== undefined && !l.used.has(abort) && once(`SYG633:${name}:${abort}`)) {
|
|
103
|
+
devReport('SYG633', {
|
|
104
|
+
component,
|
|
105
|
+
message: `${sink} { abort: '${abort}' } cancels the requests with key '${abort}', but ${name} sends its ok: '${abort}' requests with key: '${other}', so nothing is cancelled`,
|
|
106
|
+
fix: `Cancel by that key: { abort: true, key: '${other}' } (or drop the key, so the lane is the ok action)`,
|
|
107
|
+
data: {sink, abort, key: other},
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
if (typeof ok === 'string' || typeof error === 'string') {
|
|
113
|
+
const lane = key ?? ok ?? error
|
|
114
|
+
l.used.add(lane)
|
|
115
|
+
if (typeof ok === 'string' && key !== undefined && key !== ok) l.keyed.set(ok, String(key))
|
|
116
|
+
}
|
|
117
|
+
checkOne(component, sink, req, `A ${sink} request`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const fetchCheck: DiagnosticCheck = {
|
|
121
|
+
id: 'fetch',
|
|
122
|
+
|
|
123
|
+
onModel(component) {
|
|
124
|
+
const model$ = component?.model$
|
|
125
|
+
if (!model$ || typeof model$ !== 'object') return
|
|
126
|
+
for (const sink of Object.keys(model$)) {
|
|
127
|
+
const s$ = model$[sink]
|
|
128
|
+
if (!fetchSink(component, sink) || !s$ || typeof s$.map !== 'function') continue
|
|
129
|
+
model$[sink] = s$.map((req: any) => {
|
|
130
|
+
try { check(component, sink, req) } catch (_) {}
|
|
131
|
+
return req
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-5 F-1: `form` behavior diagnostics (dev entry; src/extra/form.ts carries no text).
|
|
3
|
+
*
|
|
4
|
+
* The form module calls `__SYGNAL_DIAGNOSTICS__.form(code, ...args)`:
|
|
5
|
+
* 231 form(schema) with a schema that isn't a Standard Schema: throws the SYG231 Error (in
|
|
6
|
+
* production the first validation throws a plain TypeError)
|
|
7
|
+
* 230 (name, values) an input / focusout from a named field whose name isn't in `values`:
|
|
8
|
+
* SYG230 (warn, once per name); the event is ignored
|
|
9
|
+
* 232 (slice) a submit while submitting or while a queued submit waits for a check: SYG232
|
|
10
|
+
* (info, every time); the submit is dropped
|
|
11
|
+
* 233 (values, output) a valid submit: each top-level `values` key missing from the schema's
|
|
12
|
+
* output, SYG233 (warn, once per key): the schema strips it (zod's z.object does). G-377:
|
|
13
|
+
* only when every output key is a values key (an output with other keys reshapes: a rename)
|
|
14
|
+
* 235 (field, request) a check's request with `ok` / `error` / `latest` keys, which the form
|
|
15
|
+
* overwrites: SYG235 (warn, once per field)
|
|
16
|
+
* 234 (submit, key) G-577: a valid submit whose `submit` names one of the form's own actions,
|
|
17
|
+
* so it dispatched '<key>.<submit>' instead of the host's action: SYG234 (warn, once per
|
|
18
|
+
* key and name), at the submit (the form's own action then often throws: SYG214 / SYG216)
|
|
19
|
+
* Host checks (onModel, once per component name), for each `uses` entry made by form():
|
|
20
|
+
* SYG234 (warn) `submit` missing, not a host model entry, or one of the form's own action names
|
|
21
|
+
* (G-577: every action in that form's model, CHECKED_<field> included)
|
|
22
|
+
* SYG235 (warn) a `check` key that isn't a field of `values`
|
|
23
|
+
* SYG236 (warn) an array of objects in `values` with a row without an `id`
|
|
24
|
+
* SYG237 (warn) two form uses of one host with the same form selector (G-374)
|
|
25
|
+
*/
|
|
26
|
+
import type {DiagnosticCheck} from '../index'
|
|
27
|
+
import {devReport, once, nameOf} from './shared'
|
|
28
|
+
import {docsUrlFor} from '../codes'
|
|
29
|
+
import {hasField} from '../../formHelpers'
|
|
30
|
+
|
|
31
|
+
// the form's own action names (G-577: read from the form's model, so CHECKED_<field> counts)
|
|
32
|
+
const ownActions = (b: any): string[] => Object.keys(b?.model || {})
|
|
33
|
+
const SUBMIT_FIX = (key: string, a: string, own: string[]) =>
|
|
34
|
+
`Name the submit after a host action the form doesn't define (not ${own.join(', ')}): submit: 'SAVE', and rename the model entry '${a}' to SAVE: { HTTP: (state, values) => ({ url, json: values, ok: '${key}.DONE', error: '${key}.ERRORS' }) }`
|
|
35
|
+
const RESERVED = ['ok', 'error', 'latest']
|
|
36
|
+
|
|
37
|
+
const what = (v: any): string =>
|
|
38
|
+
v === null || v === undefined ? String(v)
|
|
39
|
+
: typeof v == 'function' ? 'a function'
|
|
40
|
+
: Array.isArray(v) ? 'an array'
|
|
41
|
+
: typeof v == 'object' ? `an object without ~standard.validate${typeof v.parse == 'function' ? ' (a validator from before Standard Schema?)' : ''}`
|
|
42
|
+
: `a ${typeof v}`
|
|
43
|
+
|
|
44
|
+
export function reportForm(code: number, a?: any, b?: any, c?: any): any {
|
|
45
|
+
if (code == 231) {
|
|
46
|
+
const err: any = new TypeError(`[Sygnal SYG231] form(schema, options): the schema is ${what(a)}, not a Standard Schema. ` +
|
|
47
|
+
`Pass a Standard Schema object: a zod, valibot or arktype schema, or an object with '~standard': { version: 1, vendor, validate }. ${docsUrlFor('SYG231')}`)
|
|
48
|
+
err.code = 'SYG231'
|
|
49
|
+
throw err
|
|
50
|
+
}
|
|
51
|
+
if (code == 230) {
|
|
52
|
+
if (!once(`SYG230:${a}`)) return
|
|
53
|
+
devReport('SYG230', {
|
|
54
|
+
component: 'form',
|
|
55
|
+
message: `form: a field named '${a}' changed, but '${a}' isn't a field of the form's values (${Object.keys(b || {}).join(', ') || 'none'}); the change is ignored`,
|
|
56
|
+
fix: `Name the input after a path in values (name="email", name="addresses.<row id>.city"), add '${a}' to values, or move the input out of the form element`,
|
|
57
|
+
data: {name: a},
|
|
58
|
+
})
|
|
59
|
+
} else if (code == 232) {
|
|
60
|
+
devReport('SYG232', {
|
|
61
|
+
component: 'form',
|
|
62
|
+
message: `form: a submit was dropped: ${a?.submitting ? 'the previous submit is still being sent (no form.DONE / form.ERRORS yet)' : 'a submit is already queued, waiting for an async check or an async schema'}`,
|
|
63
|
+
fix: `Disable the submit button while state.form.submitting (or queued) is true; answer the submit with ok: 'form.DONE', error: 'form.ERRORS'`,
|
|
64
|
+
})
|
|
65
|
+
} else if (code == 233) {
|
|
66
|
+
if (!b || typeof b != 'object' || Object.keys(b).some(k => !(k in (a || {})))) return
|
|
67
|
+
for (const k of Object.keys(a || {})) {
|
|
68
|
+
if (!(k in b) && once(`SYG233:${k}`)) devReport('SYG233', {
|
|
69
|
+
component: 'form',
|
|
70
|
+
message: `form: values.${k} is not in the schema's output (stripped or renamed), so the submit action doesn't get it under that name (a schema strips keys it doesn't declare)`,
|
|
71
|
+
fix: `Declare '${k}' in the schema (or make the schema keep unknown keys), or remove it from values; if the schema renames it on purpose, ignore this`,
|
|
72
|
+
data: {key: k},
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
} else if (code == 234) {
|
|
76
|
+
if (!once(`SYG234:run:${b}:${a}`)) return
|
|
77
|
+
devReport('SYG234', {
|
|
78
|
+
component: 'form',
|
|
79
|
+
message: `form '${b}': a valid submit dispatched '${b}.${a}' (the form's own ${a} action), not the host's '${a}': submit: '${a}' names one of the form's own actions, which next() prefers`,
|
|
80
|
+
fix: SUBMIT_FIX(b, a, ownActions(c)),
|
|
81
|
+
data: {key: b, submit: a},
|
|
82
|
+
})
|
|
83
|
+
} else if (code == 235) {
|
|
84
|
+
const set = RESERVED.filter(k => b && k in b)
|
|
85
|
+
if (set.length && once(`SYG235:req:${a}`)) devReport('SYG235', {
|
|
86
|
+
component: 'form',
|
|
87
|
+
message: `form: the check for '${a}' returns a request with ${set.map(k => `'${k}'`).join(', ')}; the form sets them itself (its reply action and latest: true), so they are replaced`,
|
|
88
|
+
fix: `Return only the request: check: { ${a}: { request: (v) => ({ url, query }), error: (body) => message } }`,
|
|
89
|
+
data: {field: a, keys: set},
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** row ids missing in `values`: the names of the arrays of objects that have a row without an id */
|
|
95
|
+
const missingIds = (v: any, at = '', out: string[] = []): string[] => {
|
|
96
|
+
if (v && typeof v == 'object') {
|
|
97
|
+
if (Array.isArray(v) && v.some(r => r && typeof r == 'object' && !Array.isArray(r) && r.id == null)) out.push(at)
|
|
98
|
+
for (const k in v) missingIds(v[k], at ? `${at}.${k}` : k, out)
|
|
99
|
+
}
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const formsCheck: DiagnosticCheck = {
|
|
104
|
+
id: 'forms',
|
|
105
|
+
|
|
106
|
+
onModel(component) {
|
|
107
|
+
const view = component?.view, uses = view?.uses
|
|
108
|
+
if (!uses || typeof uses != 'object') return
|
|
109
|
+
const name = nameOf(component), sels: Record<string, string> = {}
|
|
110
|
+
for (const key of Object.keys(uses)) {
|
|
111
|
+
const b = uses[key]
|
|
112
|
+
if (!b?.form) continue
|
|
113
|
+
// G-374: each form listens on its selector inside the host, so two with one selector hear
|
|
114
|
+
// each other's fields and submits
|
|
115
|
+
const sel = b.options?.form || 'form'
|
|
116
|
+
if (sel in sels) {
|
|
117
|
+
if (once(`SYG237:${name}:${sel}`)) devReport('SYG237', {
|
|
118
|
+
component,
|
|
119
|
+
message: `${name}'s forms '${sels[sel]}' and '${key}' both listen on '${sel}', so each hears the other's fields and submits`,
|
|
120
|
+
fix: `Give each form element its own class and pass it: ${sels[sel]}: form(schema, { ..., form: '.${sels[sel]}' }), ${key}: form(schema, { ..., form: '.${key}' })`,
|
|
121
|
+
data: {keys: [sels[sel], key], form: sel},
|
|
122
|
+
})
|
|
123
|
+
} else sels[sel] = key
|
|
124
|
+
if (!once(`SYG23x:${name}:${key}`)) continue
|
|
125
|
+
const o = b.options || {}, values = o.values || {}
|
|
126
|
+
const model = Object.keys(view.model || {}).map(a => a.split('|')[0].trim()), own = ownActions(b)
|
|
127
|
+
const clash = own.includes(o.submit)
|
|
128
|
+
if (typeof o.submit != 'string' || !model.includes(o.submit) || clash) {
|
|
129
|
+
devReport('SYG234', {
|
|
130
|
+
component,
|
|
131
|
+
message: typeof o.submit != 'string' ? `${name}'s form '${key}' has no submit action, so a valid submit dispatches nothing`
|
|
132
|
+
: clash ? `${name}'s form '${key}' names submit: '${o.submit}', one of the form's own actions; a valid submit dispatches the form's '${key}.${o.submit}'${model.includes(o.submit) ? `, never ${name}'s '${o.submit}' entry` : ''}`
|
|
133
|
+
: `${name}'s form '${key}' dispatches '${o.submit}' on a valid submit, but ${name} has no model entry '${o.submit}'`,
|
|
134
|
+
fix: clash ? SUBMIT_FIX(key, o.submit, own)
|
|
135
|
+
: `Name a host action and add it to the model: submit: 'SIGN_UP', model: { SIGN_UP: { HTTP: (state, values) => ({ url, json: values, ok: '${key}.DONE', error: '${key}.ERRORS' }) } }`,
|
|
136
|
+
data: {key, submit: o.submit},
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
for (const f of Object.keys(o.check || {})) {
|
|
140
|
+
if (!hasField(values, f)) devReport('SYG235', {
|
|
141
|
+
component,
|
|
142
|
+
message: `${name}'s form '${key}' has a check for '${f}', which isn't a field of its values; it never runs`,
|
|
143
|
+
fix: `Use a field name from values (${Object.keys(values).join(', ')})`,
|
|
144
|
+
data: {key, field: f},
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
for (const at of missingIds(values)) devReport('SYG236', {
|
|
148
|
+
component,
|
|
149
|
+
message: `${name}'s form '${key}': values.${at} has rows without an id, so its fields are named by index and errors, touched state and focus don't follow a row when another is removed`,
|
|
150
|
+
fix: `Give each row a unique id: ${at}: [{ id: 1, ... }] (form.ADD numbers new rows itself)`,
|
|
151
|
+
data: {key, field: at},
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Install the `form` hook on the core bridge (form.ts calls it). */
|
|
158
|
+
export function installFormHooks(): () => void {
|
|
159
|
+
const c = (globalThis as any).__SYGNAL_DIAGNOSTICS__
|
|
160
|
+
if (!c) return () => {}
|
|
161
|
+
c.form = reportForm
|
|
162
|
+
return () => { if (c.form === reportForm) c.form = undefined }
|
|
163
|
+
}
|