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
|
@@ -34,9 +34,8 @@ export interface DiagnosticCheck {
|
|
|
34
34
|
* Strict mode (canonical forms, SYG5xx): turn the runtime strict checks on
|
|
35
35
|
* (true), off (false), or back to the default (undefined → on only when
|
|
36
36
|
* globalThis.__SYGNAL_STRICT__ === true). Off by default. Diagnostics must
|
|
37
|
-
* also be on. Runtime rules: SYG501 (positional view args),
|
|
38
|
-
*
|
|
39
|
-
* keys); `sygnal-check --strict` checks all of SYG501-507 statically.
|
|
37
|
+
* also be on. Runtime rules: SYG501 (positional view args), SYG504 ('ACTION | SINK'
|
|
38
|
+
* keys); `sygnal-check --strict` checks SYG501-508 statically (SYG502 is retired in 6.0).
|
|
40
39
|
* In tests: renderComponent(C, { strict: true }).
|
|
41
40
|
*/
|
|
42
41
|
export function configureStrict(on?: boolean): void
|
|
@@ -96,15 +95,18 @@ export function getCodeInfo(code: string): DiagnosticCodeInfo | undefined
|
|
|
96
95
|
// ---------------------------------------------------------------------------
|
|
97
96
|
|
|
98
97
|
/** How an action is dispatched. */
|
|
99
|
-
export type InspectActionTrigger = 'intent' | 'next' | 'builtin' | 'unknown'
|
|
98
|
+
export type InspectActionTrigger = 'intent' | 'next' | 'reply' | 'builtin' | 'unknown'
|
|
100
99
|
|
|
101
100
|
export interface InspectAction {
|
|
102
101
|
name: string
|
|
103
102
|
/**
|
|
104
103
|
* 'intent': returned by the component's intent. 'builtin': BOOTSTRAP,
|
|
105
|
-
* INITIALIZE,
|
|
106
|
-
* (
|
|
107
|
-
*
|
|
104
|
+
* INITIALIZE, DISPOSE or READY. 'reply': named as a reply action by a request
|
|
105
|
+
* (`ok: 'NAME'` / `error: 'NAME'`) or a `connections` entry (statically: a
|
|
106
|
+
* string literal; at runtime: a request the instance was seen sending).
|
|
107
|
+
* 'next': dispatched with next() (statically: a next('NAME') literal; at
|
|
108
|
+
* runtime: a model-only action whose STATE reducer was seen running).
|
|
109
|
+
* 'unknown': none of these is known.
|
|
108
110
|
*/
|
|
109
111
|
trigger: InspectActionTrigger
|
|
110
112
|
/** sinks of the model entry (STATE, EVENTS, EFFECT, PARENT, custom drivers); [] without a model entry */
|
|
@@ -134,6 +136,20 @@ export interface InspectSelector {
|
|
|
134
136
|
matched: boolean | null
|
|
135
137
|
/** the child component whose (isolated) elements it matches instead, if any */
|
|
136
138
|
isolationHit: string | null
|
|
139
|
+
/** PLAN-4 CT-1: the control this selector is (its key), when it is `[data-control="<Key>"]` */
|
|
140
|
+
control?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** PLAN-4 CT-1: a control (controls()) a component renders */
|
|
144
|
+
export interface InspectControl {
|
|
145
|
+
/** the control's key (rendered as data-control="<name>") */
|
|
146
|
+
name: string
|
|
147
|
+
/** the intrinsic tag of a tag spec; null for a spec object */
|
|
148
|
+
element: string | null
|
|
149
|
+
/** a spec object's kind (e.g. 'widget') */
|
|
150
|
+
kind?: string
|
|
151
|
+
/** whether the component's intent listens to it; null when unknown */
|
|
152
|
+
listened: boolean | null
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
export interface InspectDiagnostic {
|
|
@@ -178,7 +194,60 @@ export interface InspectComponent {
|
|
|
178
194
|
eventsSelected: string[]
|
|
179
195
|
children: InspectChild[]
|
|
180
196
|
selectors: InspectSelector[]
|
|
197
|
+
/** PLAN-4 CT-1: the controls it renders (runtime: seen in its renders); omitted when none */
|
|
198
|
+
controls?: InspectControl[]
|
|
199
|
+
/** static only, PLAN-4 GS-2 (G-224): the element commands (ELEMENT sink) its model sends; omitted when none */
|
|
200
|
+
commands?: InspectCommand[]
|
|
201
|
+
/** static only, PLAN-4 GS-7 (G-224): the literal timer specs of its `timers` static; omitted when none */
|
|
202
|
+
timers?: InspectTimer[]
|
|
181
203
|
diagnostics: InspectDiagnostic[]
|
|
204
|
+
/** runtime, PLAN-3 5-3: the instance's `resources` and their state */
|
|
205
|
+
resources?: InspectResource[]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** PLAN-4 GS-2: an element command a model entry sends (static inspect, sygnal-check --graph) */
|
|
209
|
+
export interface InspectCommand {
|
|
210
|
+
/** the model entry that sends it */
|
|
211
|
+
action: string
|
|
212
|
+
/** the command's method (its first key) */
|
|
213
|
+
method: string
|
|
214
|
+
/** the control's key or the selector; null when not static */
|
|
215
|
+
target: string | null
|
|
216
|
+
/** the control it targets (its key) */
|
|
217
|
+
control?: string
|
|
218
|
+
/** intent actions listening on the target for a native event the command causes (close, toggle...) */
|
|
219
|
+
triggers?: string[]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** PLAN-4 GS-7: a literal timer spec of a `timers` static (static inspect, sygnal-check --graph) */
|
|
223
|
+
export interface InspectTimer {
|
|
224
|
+
name: string
|
|
225
|
+
every?: number | null
|
|
226
|
+
after?: number | null
|
|
227
|
+
/** the action a frame timer dispatches */
|
|
228
|
+
frame?: string | null
|
|
229
|
+
action?: string | null
|
|
230
|
+
background?: true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** PLAN-3 5-3: a resource of a component instance (runtime inspect()) */
|
|
234
|
+
export interface InspectResource {
|
|
235
|
+
name: string
|
|
236
|
+
status: 'idle' | 'loading' | 'success' | 'error'
|
|
237
|
+
refreshing: boolean
|
|
238
|
+
/** `data` is set (a success, or kept through a refetch) */
|
|
239
|
+
hasData: boolean
|
|
240
|
+
/** the error message while 'error' */
|
|
241
|
+
error?: string
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** PLAN-3 5-3: a makeFetchDriver cache entry (runtime inspect(), t.cache) */
|
|
245
|
+
export interface InspectCacheEntry {
|
|
246
|
+
key: string
|
|
247
|
+
age?: number
|
|
248
|
+
stale: boolean
|
|
249
|
+
subscribers: number
|
|
250
|
+
data: any
|
|
182
251
|
}
|
|
183
252
|
|
|
184
253
|
export interface InspectGraph {
|
|
@@ -190,6 +259,26 @@ export interface InspectGraph {
|
|
|
190
259
|
events: Record<string, { emitters: string[]; selectors: string[] }>
|
|
191
260
|
/** diagnostics not tied to a listed component */
|
|
192
261
|
diagnostics: InspectDiagnostic[]
|
|
262
|
+
/** runtime, PLAN-3 5-3: makeFetchDriver cache entries, by sink name (empty when no cache) */
|
|
263
|
+
cache?: Record<string, InspectCacheEntry[]>
|
|
264
|
+
/** runtime, PLAN-4 2-C (GS-10): with `inspect({ actions })`, the most recent actions, oldest first */
|
|
265
|
+
recentActions?: InspectRecentAction[]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** PLAN-4 2-C (GS-10): one recent action (inspect({ actions })) */
|
|
269
|
+
export interface InspectRecentAction {
|
|
270
|
+
type: string
|
|
271
|
+
/** the action's data, when it is JSON-safe and small (a DOM event is left out) */
|
|
272
|
+
data?: any
|
|
273
|
+
/** the component's name */
|
|
274
|
+
component: string
|
|
275
|
+
/** the instance's id (InspectComponent.id) */
|
|
276
|
+
instance: string
|
|
277
|
+
/** the sinks that produced a value for it (not ABORT) */
|
|
278
|
+
sinks: string[]
|
|
279
|
+
cause: 'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior'
|
|
280
|
+
/** ms since the dev entry started recording (or the last resetChecks()) */
|
|
281
|
+
at: number
|
|
193
282
|
}
|
|
194
283
|
|
|
195
284
|
export interface InspectOptions {
|
|
@@ -199,6 +288,8 @@ export interface InspectOptions {
|
|
|
199
288
|
selectors?: Record<string, InspectSelector[]>
|
|
200
289
|
/** diagnostics to attach (default: the devtools' collected diagnostics, when available) */
|
|
201
290
|
diagnostics?: Array<{ code: string; severity: DiagnosticSeverity; component?: string; message: string; [key: string]: any }>
|
|
291
|
+
/** PLAN-4 2-C: add `recentActions`: true for every kept one (the last 200), a number for the last n */
|
|
292
|
+
actions?: boolean | number
|
|
202
293
|
}
|
|
203
294
|
|
|
204
295
|
/**
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reply actions (PLAN-3 §1.1, workstream 1-D).
|
|
3
|
+
*
|
|
4
|
+
* SYG112 — a request names a reply action the sending component has no
|
|
5
|
+
* model entry for (error: the reply is dropped). Dev-entry code
|
|
6
|
+
* (DEV_CODE_SEVERITY).
|
|
7
|
+
* SYG508 — strict: the component reads the replies to its own request with
|
|
8
|
+
* X.select('cat') / X.errors('cat') where reply actions would do.
|
|
9
|
+
*
|
|
10
|
+
* Mechanism:
|
|
11
|
+
* onSink (checks/next.ts: checkRequest) every value an instance sends to a
|
|
12
|
+
* reply-capable sink (`__sygnalReplies === true`: makeFetchDriver,
|
|
13
|
+
* driverFromAsync, the socket driver) is checked as it is sent.
|
|
14
|
+
* The names it sees feed inspect()'s 'reply' trigger.
|
|
15
|
+
* sources (strict only) wraps those sources for the intent, recording the
|
|
16
|
+
* select()/errors() categories the instance reads.
|
|
17
|
+
* A request's `category` is compared for SYG508 (driverFromAsync's custom
|
|
18
|
+
* `selector` property is not known at run time; sygnal-check covers it).
|
|
19
|
+
* Reported once per component name and action / category.
|
|
20
|
+
*
|
|
21
|
+
* `{ connections }` values (the `connections` static, which the core sends to
|
|
22
|
+
* the same sink, or one a model entry sends) are checked the same way:
|
|
23
|
+
* each connection's message / open / close / error and its SSE `events` names.
|
|
24
|
+
*/
|
|
25
|
+
import type {DiagnosticCheck} from '../index'
|
|
26
|
+
import {STRICT_CODE_SEVERITY} from '../codes'
|
|
27
|
+
import {devReport, reportSafely, once, nameOf, suggest, replySeen} from './shared'
|
|
28
|
+
import {isStrictEnabled} from './strict'
|
|
29
|
+
|
|
30
|
+
const REPLY_KEYS = ['ok', 'error']
|
|
31
|
+
// PLAN-3 5-4b: on the router's sink, `{ route }` (the route static) and `{ block }` name reply actions
|
|
32
|
+
const ROUTER_KEYS = ['route', 'block']
|
|
33
|
+
const CONNECTION_KEYS = ['message', 'open', 'close', 'error']
|
|
34
|
+
|
|
35
|
+
/** [action, key, connection name?] for every reply action name a sink value names */
|
|
36
|
+
function replyNames(req: any, router?: boolean): Array<[string, string, string?]> {
|
|
37
|
+
const out: Array<[string, string, string?]> = []
|
|
38
|
+
for (const key of router ? REPLY_KEYS.concat(ROUTER_KEYS) : REPLY_KEYS) if (typeof req[key] === 'string') out.push([req[key], key])
|
|
39
|
+
const conns = req.connections
|
|
40
|
+
if (conns && typeof conns === 'object') {
|
|
41
|
+
for (const name of Object.keys(conns)) {
|
|
42
|
+
const spec = conns[name]
|
|
43
|
+
if (!spec || typeof spec !== 'object') continue
|
|
44
|
+
for (const key of CONNECTION_KEYS) if (typeof spec[key] === 'string') out.push([spec[key], key, name])
|
|
45
|
+
const events = spec.events
|
|
46
|
+
if (events && typeof events === 'object') {
|
|
47
|
+
for (const ev of Object.keys(events)) if (typeof events[ev] === 'string') out.push([events[ev], `events.${ev}`, name])
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** instance → 'SINK\0category' → the select/errors call that read it */
|
|
55
|
+
const selected = new WeakMap<object, Map<string, string>>()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
function readingSource(component: any, sink: string, source: any) {
|
|
59
|
+
const wrapped = Object.create(source)
|
|
60
|
+
for (const method of ['select', 'errors']) {
|
|
61
|
+
if (typeof source[method] !== 'function') continue
|
|
62
|
+
wrapped[method] = (category?: any, ...rest: any[]) => {
|
|
63
|
+
if (typeof category === 'string') {
|
|
64
|
+
let reads = selected.get(component)
|
|
65
|
+
if (!reads) selected.set(component, reads = new Map())
|
|
66
|
+
reads.set(`${sink}\0${category}`, `${sink}.${method}('${category}')`)
|
|
67
|
+
}
|
|
68
|
+
return source[method](category, ...rest)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return wrapped
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function checkRequest(component: any, sink: string, req: any, modelActions: string[]) {
|
|
75
|
+
if (!req || typeof req !== 'object') return
|
|
76
|
+
const name = nameOf(component)
|
|
77
|
+
let replies = false
|
|
78
|
+
for (const [action, key, connection] of replyNames(req, !!component?.sources?.[sink]?.__sygnalRouter)) {
|
|
79
|
+
replies = true
|
|
80
|
+
let seen = replySeen.get(component)
|
|
81
|
+
if (!seen) replySeen.set(component, seen = new Set())
|
|
82
|
+
seen.add(action)
|
|
83
|
+
if (modelActions.includes(action) || !once(`SYG112:${name}:${action}`)) continue
|
|
84
|
+
const near = suggest(action, modelActions.filter(a => !a.startsWith('__')))
|
|
85
|
+
devReport('SYG112', {
|
|
86
|
+
component,
|
|
87
|
+
message: (connection
|
|
88
|
+
? `${sink} connection '${connection}' names '${action}' as its ${key} reply action, but ${name} has no model entry '${action}', so those events are dropped`
|
|
89
|
+
: `A ${sink} request names '${action}' as its ${key} reply action, but ${name} has no model entry '${action}', so the reply is dropped`) +
|
|
90
|
+
(near ? ` (did you mean '${near}'?)` : ''),
|
|
91
|
+
fix: near
|
|
92
|
+
? `Use the existing entry: ${key.startsWith('events.') ? `events: { '${key.slice(7)}': '${near}' }` : `${key}: '${near}'`}`
|
|
93
|
+
: `Add '${action}' to ${name}.model, e.g. ${action}: (state, data) => ({ ...state, ... }), or fix the name`,
|
|
94
|
+
data: {action, key, sink, ...(connection ? {connection} : {}), ...(near ? {suggestion: near} : {}), modelActions},
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// SYG508 (strict): a request without reply actions whose replies this instance reads back with select()/errors()
|
|
99
|
+
if (replies || 'abort' in req || typeof req.category !== 'string' || !isStrictEnabled()) return
|
|
100
|
+
const read = selected.get(component)?.get(`${sink}\0${req.category}`)
|
|
101
|
+
if (!read || !once(`SYG508:${name}:${sink}:${req.category}`)) return
|
|
102
|
+
// (inside a sink stream: reportSafely, so 'error' mode can't break the stream)
|
|
103
|
+
reportSafely('SYG508', {
|
|
104
|
+
severity: STRICT_CODE_SEVERITY.SYG508,
|
|
105
|
+
component,
|
|
106
|
+
message: `${name} sends a ${sink} request with category: '${req.category}' and reads the reply back with ${read}`,
|
|
107
|
+
fix: `Use reply actions instead: name them in the request, ${sink}: (state) => ({ url, ok: 'LOADED', error: 'FAILED' }), ` +
|
|
108
|
+
`handle LOADED (the parsed body) and FAILED ({ error, status, body, request }) in the model, and remove ${read} from the intent`,
|
|
109
|
+
data: {sink, category: req.category, read},
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const repliesCheck: DiagnosticCheck = {
|
|
114
|
+
id: 'replies',
|
|
115
|
+
|
|
116
|
+
sources(component, sources) {
|
|
117
|
+
if (!isStrictEnabled() || typeof Proxy != 'function') return
|
|
118
|
+
const cache = new Map<string, any>()
|
|
119
|
+
return new Proxy(sources, {
|
|
120
|
+
get(t: any, k: any) {
|
|
121
|
+
const v = t[k]
|
|
122
|
+
if (typeof k != 'string' || !v || v.__sygnalReplies !== true) return v
|
|
123
|
+
if (!cache.has(k)) cache.set(k, readingSource(component, k, v))
|
|
124
|
+
return cache.get(k)
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
},
|
|
128
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router and declaration statics (PLAN-3 5-4b). Dev-entry-only codes (DEV_CODE_SEVERITY):
|
|
3
|
+
*
|
|
4
|
+
* SYG130 — href() names no route, or leaves out a param its pattern needs (the link is broken).
|
|
5
|
+
* (A `{ to }` command with the same problem is SYG620, reported by the router itself
|
|
6
|
+
* in production too, and not navigated.)
|
|
7
|
+
* SYG131 — href() or a `{ to, params }` command passes params the route's pattern doesn't use.
|
|
8
|
+
* SYG132 — G-167: a component declares a static the core sends to a driver (`route`,
|
|
9
|
+
* `resources`, `connections`, `head`) but never sends it: it has no model (the core
|
|
10
|
+
* wires statics in its model setup), or it is a root without initialState (a static is
|
|
11
|
+
* computed from the state stream, which a root without state never emits).
|
|
12
|
+
* SYG133 — the SPA router (makeRouter().driver without `navigate`) runs inside a Vike app,
|
|
13
|
+
* where Vike owns links and history.
|
|
14
|
+
*
|
|
15
|
+
* Mechanism: the router's href() calls `__SYGNAL_DIAGNOSTICS__.routerHref(routes, name, params)`
|
|
16
|
+
* when it exists (installRouterHooks() sets it); onModel looks at the component's sources for
|
|
17
|
+
* `__sygnalStatic` / `__sygnalRouter` markers.
|
|
18
|
+
*/
|
|
19
|
+
import type {DiagnosticCheck} from '../index'
|
|
20
|
+
import {bridge, devReport, once, nameOf, suggest} from './shared'
|
|
21
|
+
|
|
22
|
+
const PARAM = /:(\w+)/g
|
|
23
|
+
const paramsOf = (pat: string) => (pat.match(PARAM) || []).map(p => p.slice(1))
|
|
24
|
+
|
|
25
|
+
function checkHref(routes: Record<string, string>, name: string, params: any) {
|
|
26
|
+
const names = Object.keys(routes).filter(k => routes[k] != '*')
|
|
27
|
+
const pat = routes[name]
|
|
28
|
+
if (pat == null || pat == '*') {
|
|
29
|
+
if (!once(`SYG130:${name}`)) return
|
|
30
|
+
const near = suggest(String(name), names)
|
|
31
|
+
devReport('SYG130', {
|
|
32
|
+
message: `href('${name}') names no route, so the link points to '/'` + (near ? ` (did you mean '${near}'?)` : ''),
|
|
33
|
+
fix: `Use one of the route names: ${names.join(', ')}`,
|
|
34
|
+
data: {name, routes: names, ...(near ? {suggestion: near} : {})},
|
|
35
|
+
})
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
const wanted = paramsOf(pat)
|
|
39
|
+
const given = params && typeof params == 'object' ? params : {}
|
|
40
|
+
const missing = wanted.filter(k => given[k] == null)
|
|
41
|
+
const extra = Object.keys(given).filter(k => !wanted.includes(k))
|
|
42
|
+
if (missing.length && once(`SYG130:${name}:${missing}`)) {
|
|
43
|
+
devReport('SYG130', {
|
|
44
|
+
message: `href('${name}') leaves out ${missing.map(k => `'${k}'`).join(', ')} (${pat}), so that segment is empty`,
|
|
45
|
+
fix: `href('${name}', { ${wanted.join(', ')} })`,
|
|
46
|
+
data: {name, pattern: pat, missing},
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
if (extra.length && once(`SYG131:${name}:${extra}`)) {
|
|
50
|
+
devReport('SYG131', {
|
|
51
|
+
message: `Route '${name}' (${pat}) has no param ${extra.map(k => `'${k}'`).join(', ')}; it is ignored`,
|
|
52
|
+
fix: wanted.length ? `Pass only ${wanted.join(', ')}; put other values in the query: href('${name}', params, { ${extra[0]} })` : `Put it in the query: href('${name}', {}, { ${extra[0]} })`,
|
|
53
|
+
data: {name, pattern: pat, extra},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** install the hook the router's href() calls (removed by the returned function) */
|
|
59
|
+
export function installRouterHooks(): () => void {
|
|
60
|
+
const core = bridge()
|
|
61
|
+
if (!core) return () => {}
|
|
62
|
+
core.routerHref = checkHref
|
|
63
|
+
return () => { if (core.routerHref === checkHref) core.routerHref = undefined }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const routerCheck: DiagnosticCheck = {
|
|
67
|
+
id: 'router',
|
|
68
|
+
|
|
69
|
+
onModel(component) {
|
|
70
|
+
const sources = component?.sources
|
|
71
|
+
if (!sources || typeof sources != 'object') return
|
|
72
|
+
const name = nameOf(component)
|
|
73
|
+
for (const n of component.sourceNames || Object.keys(sources)) {
|
|
74
|
+
const src = sources[n]
|
|
75
|
+
const k = src?.__sygnalStatic
|
|
76
|
+
if (typeof k == 'string' && component.view?.[k] != null) {
|
|
77
|
+
// statics are computed from state (G-167: with or without a model): a root whose state
|
|
78
|
+
// never emits (a model but no initialState) never sends one
|
|
79
|
+
if (!component.isSubComponent && component.initialState === undefined && once(`SYG132:${name}:${k}`)) {
|
|
80
|
+
devReport('SYG132', {
|
|
81
|
+
component,
|
|
82
|
+
message: `${name} declares ${name}.${k}, but a root without initialState has no state, so ${k} is never sent to ${n}`,
|
|
83
|
+
fix: `Add ${name}.initialState (e.g. { }${k == 'route' ? ', or { route: router.current() }' : ''})`,
|
|
84
|
+
data: {static: k, source: n, reason: 'state'},
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const opts = src?.__sygnalRouter
|
|
89
|
+
const g: any = globalThis
|
|
90
|
+
if (opts && typeof opts == 'object' && !opts.navigate && g.document?.getElementById?.('vike_pageContext') && once('SYG133')) {
|
|
91
|
+
devReport('SYG133', {
|
|
92
|
+
component,
|
|
93
|
+
message: `The SPA router (${n}) runs inside a Vike app: Vike owns links and history, so both would handle a click`,
|
|
94
|
+
fix: "Pass Vike's navigate: makeRouter({ routes, navigate }) with import { navigate } from 'vike/client/router', or use Vike's routing",
|
|
95
|
+
data: {source: n},
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* xstream equivalent and the SYG301 docs link. The error is also reported
|
|
10
10
|
* (collected / printed per the diagnostics mode; in 'error' mode report()
|
|
11
11
|
* throws the DiagnosticError itself). It works anywhere a stream is used
|
|
12
|
-
* (intent, peers, drivers), with no hook in
|
|
12
|
+
* (intent, peers, drivers), with no hook in the core.
|
|
13
13
|
*
|
|
14
14
|
* Caveat: `typeof stream.pipe === 'function'` is true while this entry is
|
|
15
15
|
* loaded (dev only).
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import '../index'
|
|
13
13
|
import type {DiagnosticCheck, DiagnosticDetails, Diagnostic} from '../index'
|
|
14
|
+
import {CODE_TITLES, DEV_CODE_SEVERITY, registerCodes} from '../codes'
|
|
15
|
+
import type {DiagnosticSeverity} from '../codes'
|
|
14
16
|
|
|
15
17
|
interface CoreBridge {
|
|
16
18
|
registerCheck(check: DiagnosticCheck): () => void
|
|
@@ -38,6 +40,16 @@ export const reportSafely = (code: string, details: DiagnosticDetails): Diagnost
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
registerCodes(Object.keys(DEV_CODE_SEVERITY).map(code =>
|
|
44
|
+
[code, DEV_CODE_SEVERITY[code], CODE_TITLES[code]] as [string, DiagnosticSeverity, string]))
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* reportSafely() for the dev-entry-only codes (DEV_CODE_SEVERITY, G-143): the main bundle's
|
|
48
|
+
* core doesn't know their severity, so it is passed explicitly.
|
|
49
|
+
*/
|
|
50
|
+
export const devReport = (code: string, details: DiagnosticDetails): Diagnostic | undefined =>
|
|
51
|
+
reportSafely(code, {severity: DEV_CODE_SEVERITY[code], ...details})
|
|
52
|
+
|
|
41
53
|
/** Tunables (test seam: configureChecks()). */
|
|
42
54
|
export const timing = {
|
|
43
55
|
/** delay after a render before DOM checks run (lets nested renders patch) */
|
|
@@ -48,8 +60,76 @@ export const timing = {
|
|
|
48
60
|
minRenders: 3,
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Built-in actions Sygnal dispatches itself; never "missing" or "unreachable".
|
|
65
|
+
* (HYDRATE is not one since 6.0, D66: nothing dispatches it, so it is an ordinary action.)
|
|
66
|
+
*/
|
|
67
|
+
export const BUILTIN_ACTIONS = new Set(['BOOTSTRAP', 'INITIALIZE', 'DISPOSE', 'READY', 'RESOURCE'])
|
|
68
|
+
|
|
69
|
+
/** Sinks the core handles itself: their values never go to a reply-action driver. */
|
|
70
|
+
// (PLAN-4 GS-2: ELEMENT too; `{ scrollIntoView: Row, block: 'center' }` names no action)
|
|
71
|
+
const NON_REPLY_SINK = /^(STATE|EFFECT|EVENTS|PARENT|READY|DOM|CHILD|ELEMENT|PERSIST)$/
|
|
72
|
+
|
|
73
|
+
// `ok: 'X'` / `"error": "X"` in function source (minified code keeps string literals and keys)
|
|
74
|
+
const keyedNames = (keys: string) => new RegExp(`(?:^|[{,\\s])["']?(?:${keys})["']?\\s*:\\s*(["'\`])([\\w$.:/-]+)\\1`, 'g')
|
|
75
|
+
// (PLAN-3 5-4b: and a router `{ block: 'ACTION' }`)
|
|
76
|
+
const REPLY_IN_SOURCE = keyedNames('ok|error|block')
|
|
77
|
+
const CONNECTION_IN_SOURCE = keyedNames('message|open|close|error')
|
|
78
|
+
// G-224: a `timers` static's `action: 'TICK'` / `frame: 'FRAME'`
|
|
79
|
+
const TIMER_IN_SOURCE = keyedNames('action|frame')
|
|
80
|
+
// PLAN-5 B-3: a `browser` static's `action: 'SEEN'` / `error: 'GEO_FAILED'`
|
|
81
|
+
const BROWSER_IN_SOURCE = keyedNames('action|error')
|
|
82
|
+
|
|
83
|
+
const namesIn = (fn: any, re: RegExp, out: Set<string>) => {
|
|
84
|
+
if (typeof fn !== 'function') return
|
|
85
|
+
let src = ''
|
|
86
|
+
try { src = Function.prototype.toString.call(fn) } catch (_) { return }
|
|
87
|
+
re.lastIndex = 0
|
|
88
|
+
for (let m = re.exec(src); m; m = re.exec(src)) out.add(m[2])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Reply actions a component's requests and `connections` static can name, read from the
|
|
93
|
+
* source of its non-STATE sink functions (`HTTP: (s) => ({ url, ok: 'LOADED', error: 'FAILED' })`)
|
|
94
|
+
* and of `connections` (message/open/close/error). A heuristic for SYG102 (PLAN-3): a name
|
|
95
|
+
* built at run time is missed (SYG102 stays info, so that only costs a false hint).
|
|
96
|
+
*/
|
|
97
|
+
export function replyNamesOf(component: any): Set<string> {
|
|
98
|
+
const out = new Set<string>()
|
|
99
|
+
const model = component && component.model
|
|
100
|
+
if (model && typeof model === 'object') {
|
|
101
|
+
for (const key of Object.keys(model)) {
|
|
102
|
+
const value = model[key]
|
|
103
|
+
const bar = key.indexOf('|')
|
|
104
|
+
if (bar >= 0) {
|
|
105
|
+
if (!NON_REPLY_SINK.test(key.slice(bar + 1).trim())) namesIn(value, REPLY_IN_SOURCE, out)
|
|
106
|
+
} else if (value && typeof value === 'object') {
|
|
107
|
+
for (const sink of Object.keys(value)) {
|
|
108
|
+
if (!NON_REPLY_SINK.test(sink)) namesIn(value[sink], REPLY_IN_SOURCE, out)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
namesIn(component?.view?.connections, CONNECTION_IN_SOURCE, out)
|
|
114
|
+
// G-224: makeTimerDriver() replies the actions the `timers` static names (a function of the
|
|
115
|
+
// state, or an object of them)
|
|
116
|
+
const timers = component?.view?.timers
|
|
117
|
+
if (timers && typeof timers === 'object') for (const k in timers) namesIn(timers[k], TIMER_IN_SOURCE, out)
|
|
118
|
+
else namesIn(timers, TIMER_IN_SOURCE, out)
|
|
119
|
+
// PLAN-5 B-3: the browser driver replies the actions the `browser` static names
|
|
120
|
+
const browser = component?.view?.browser
|
|
121
|
+
if (browser && typeof browser === 'object') for (const k in browser) namesIn(browser[k], BROWSER_IN_SOURCE, out)
|
|
122
|
+
else namesIn(browser, BROWSER_IN_SOURCE, out)
|
|
123
|
+
// PLAN-3 5-4b: the router replies the action a `route` static names
|
|
124
|
+
const route = component?.view?.route
|
|
125
|
+
if (typeof route === 'string') out.add(route)
|
|
126
|
+
// PLAN-4 GS-5: persist() adds RESTORE (sent after hydration and by sync)
|
|
127
|
+
if (component?.view?.persist) out.add('RESTORE')
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Reply actions a component instance's requests were seen naming (replies.ts → inspect). */
|
|
132
|
+
export const replySeen = new WeakMap<object, Set<string>>()
|
|
53
133
|
|
|
54
134
|
/** Synthetic/internal actions (`__TEST_ACTION__`, `__NOOP_ACTION__`, ...) and built-ins. */
|
|
55
135
|
export const isInternalAction = (action: string): boolean =>
|
|
@@ -59,6 +139,10 @@ let seen = new Set<string>()
|
|
|
59
139
|
|
|
60
140
|
/** true the first time `key` is seen (dedupes reports, e.g. per component name). */
|
|
61
141
|
export const once = (key: string): boolean => (seen.has(key) ? false : (seen.add(key), true))
|
|
142
|
+
/** undo once(key): the report it guarded wasn't made (G-470: diagnostics off) */
|
|
143
|
+
export const unsee = (key: string): void => void seen.delete(key)
|
|
144
|
+
/** whether once(`key`) has reported already (a check can skip its work: SYG435) */
|
|
145
|
+
export const onced = (key: string): boolean => seen.has(key)
|
|
62
146
|
|
|
63
147
|
/**
|
|
64
148
|
* Forget only the once() dedupe set (G-051: renderComponent calls it through the core bridge
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SYG115 — unknown DOM event shorthand (warn, G-143).
|
|
3
|
+
*
|
|
4
|
+
* The core's DOM source Proxy (the DOM source wrapper) turns ANY unknown property
|
|
5
|
+
* into an event shorthand: `DOM.<name>(sel)` = `DOM.select(sel).events('<name>')`. A made-up
|
|
6
|
+
* name such as `DOM.key('.x')` or `DOM.enter('.x')` therefore listens for an event the browser
|
|
7
|
+
* never fires, and the action silently never happens.
|
|
8
|
+
*
|
|
9
|
+
* Mechanism: the `sources` hook (called before each component's intent) wraps the DOM source
|
|
10
|
+
* in a second Proxy. Calling a shorthand whose name is not a known DOM event reports SYG115
|
|
11
|
+
* (once per component name and event), then behaves exactly like the core shorthand. Only the
|
|
12
|
+
* call is reported, not the property read, so tools that inspect the source (pretty-printers)
|
|
13
|
+
* don't trigger it. `DOM.select(sel).events(name)` is never checked: it is the explicit form
|
|
14
|
+
* for custom events.
|
|
15
|
+
*/
|
|
16
|
+
import type {DiagnosticCheck} from '../index'
|
|
17
|
+
import {devReport, once, nameOf, suggest} from './shared'
|
|
18
|
+
|
|
19
|
+
// Standard DOM / UI event names (lower case). In a browser, `on<name>` on an element or the
|
|
20
|
+
// window also counts, so newer events are accepted there too.
|
|
21
|
+
const KNOWN = new Set((
|
|
22
|
+
'abort afterprint animationcancel animationend animationiteration animationstart auxclick ' +
|
|
23
|
+
'beforeinput beforeprint beforetoggle beforeunload blur cancel canplay canplaythrough change click close ' +
|
|
24
|
+
'compositionend compositionstart compositionupdate contextlost contextmenu contextrestored copy cuechange cut ' +
|
|
25
|
+
'dblclick drag dragend dragenter dragleave dragover dragstart drop durationchange emptied ended error ' +
|
|
26
|
+
'focus focusin focusout formdata fullscreenchange fullscreenerror gotpointercapture hashchange input invalid ' +
|
|
27
|
+
'keydown keypress keyup languagechange load loadeddata loadedmetadata loadstart lostpointercapture message ' +
|
|
28
|
+
'messageerror mousedown mouseenter mouseleave mousemove mouseout mouseover mouseup offline online pagehide ' +
|
|
29
|
+
'pageshow paste pause play playing pointercancel pointerdown pointerenter pointerleave pointermove pointerout ' +
|
|
30
|
+
'pointerover pointerrawupdate pointerup popstate progress ratechange rejectionhandled reset resize scroll ' +
|
|
31
|
+
'scrollend search securitypolicyviolation seeked seeking select selectionchange selectstart slotchange stalled ' +
|
|
32
|
+
'storage submit suspend timeupdate toggle touchcancel touchend touchmove touchstart transitioncancel ' +
|
|
33
|
+
'transitionend transitionrun transitionstart unhandledrejection unload visibilitychange volumechange waiting wheel'
|
|
34
|
+
).split(' '))
|
|
35
|
+
|
|
36
|
+
const isKnown = (name: string): boolean => {
|
|
37
|
+
// PLAN-5 0-S6: a hyphenated name is a custom element's event (DOM['wa-hover']('.food')): no
|
|
38
|
+
// standard DOM event has a hyphen, and a bracketed name is no typo for one
|
|
39
|
+
if (KNOWN.has(name) || name.includes('-')) return true
|
|
40
|
+
const on = 'on' + name
|
|
41
|
+
const g: any = globalThis
|
|
42
|
+
try {
|
|
43
|
+
return !!((g.HTMLElement && on in g.HTMLElement.prototype) || (g.window && on in g.window) ||
|
|
44
|
+
(g.Document && on in g.Document.prototype))
|
|
45
|
+
} catch (_) { return false }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Names agents reach for that are enriched-stream methods or key names, not events
|
|
49
|
+
const KEY_FIX = (sel: string, key?: string) =>
|
|
50
|
+
`Listen for the real event and read the key: DOM.keydown(${sel}).key()` +
|
|
51
|
+
(key ? `.filter(k => k === '${key}')` : '')
|
|
52
|
+
const SPECIAL: Record<string, (sel: string) => string> = {
|
|
53
|
+
key: sel => KEY_FIX(sel),
|
|
54
|
+
keys: sel => KEY_FIX(sel),
|
|
55
|
+
enter: sel => KEY_FIX(sel, 'Enter'),
|
|
56
|
+
escape: sel => KEY_FIX(sel, 'Escape'),
|
|
57
|
+
esc: sel => KEY_FIX(sel, 'Escape'),
|
|
58
|
+
value: sel => `Listen for the real event and read the value: DOM.input(${sel}).value()`,
|
|
59
|
+
checked: sel => `Listen for the real event and read the box: DOM.change(${sel}).checked()`,
|
|
60
|
+
data: sel => `Listen for the real event and read the attribute: DOM.click(${sel}).data('name')`,
|
|
61
|
+
doubleclick: sel => `Use DOM.dblclick(${sel})`,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const fixFor = (name: string, selector: any): string => {
|
|
65
|
+
const sel = typeof selector === 'string' ? `'${selector}'` : 'sel'
|
|
66
|
+
const special = SPECIAL[name.toLowerCase()]
|
|
67
|
+
if (special) return special(sel)
|
|
68
|
+
const s = suggest(name, KNOWN)
|
|
69
|
+
return (s ? `Did you mean DOM.${s}(${sel})? ` : '') +
|
|
70
|
+
`If '${name}' is a custom event you dispatch yourself, use the explicit form DOM.select(${sel}).events('${name}')`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const wrapped = new WeakMap<object, any>()
|
|
74
|
+
|
|
75
|
+
function checkedDOM(component: any, dom: any): any {
|
|
76
|
+
if (!dom || typeof dom != 'object') return dom
|
|
77
|
+
let proxy = wrapped.get(dom)
|
|
78
|
+
if (proxy) return proxy
|
|
79
|
+
const name = nameOf(component)
|
|
80
|
+
proxy = new Proxy(dom, {
|
|
81
|
+
get(t: any, k: any) {
|
|
82
|
+
const v = t[k]
|
|
83
|
+
if (typeof k != 'string' || k in t || typeof v != 'function' || isKnown(k)) return v
|
|
84
|
+
return function (this: any, selector: any, ...rest: any[]) {
|
|
85
|
+
if (once(`SYG115:${name}:${k}`)) {
|
|
86
|
+
devReport('SYG115', {
|
|
87
|
+
component,
|
|
88
|
+
message: `${name}.intent uses DOM.${k}(${typeof selector == 'string' ? `'${selector}'` : '…'}), but '${k}' is not a DOM event, so it never fires`,
|
|
89
|
+
fix: fixFor(k, selector),
|
|
90
|
+
data: {event: k, selector},
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
return v.call(this, selector, ...rest)
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
wrapped.set(dom, proxy)
|
|
98
|
+
return proxy
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const shorthandCheck: DiagnosticCheck = {
|
|
102
|
+
id: 'shorthand',
|
|
103
|
+
|
|
104
|
+
sources(component, sources) {
|
|
105
|
+
if (!sources || typeof Proxy != 'function') return
|
|
106
|
+
const domName = component?.DOMSourceName || 'DOM'
|
|
107
|
+
if (!sources[domName]) return
|
|
108
|
+
return new Proxy(sources, {
|
|
109
|
+
get(t: any, k: any) {
|
|
110
|
+
const v = t[k]
|
|
111
|
+
return k === domName ? checkedDOM(component, v) : v
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
}
|