sygnal 5.4.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +646 -0
- package/README.md +24 -9
- package/dist/astro/client.cjs.js +16 -9
- package/dist/astro/client.cjs.js.map +1 -1
- package/dist/astro/client.mjs +16 -9
- package/dist/astro/client.mjs.map +1 -1
- package/dist/astro/index.cjs.js +362 -91
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +362 -92
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +3396 -165
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3396 -165
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js +1431 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1419 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +3032 -296
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +3032 -296
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +257 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +381 -0
- package/dist/guide/forms.md +250 -0
- package/dist/guide/http.md +299 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +33 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +13178 -5769
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2908 -276
- package/dist/index.esm.js +13149 -5779
- package/dist/index.esm.js.map +1 -1
- package/dist/jsx-dev-runtime.cjs.js +26 -334
- package/dist/jsx-dev-runtime.cjs.js.map +1 -1
- package/dist/jsx-dev-runtime.esm.js +19 -327
- package/dist/jsx-dev-runtime.esm.js.map +1 -1
- package/dist/jsx-runtime.cjs.js +26 -334
- package/dist/jsx-runtime.cjs.js.map +1 -1
- package/dist/jsx-runtime.esm.js +19 -327
- package/dist/jsx-runtime.esm.js.map +1 -1
- package/dist/jsx.cjs.js +10 -314
- package/dist/jsx.cjs.js.map +1 -1
- package/dist/jsx.esm.js +8 -315
- package/dist/jsx.esm.js.map +1 -1
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +4 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -1
- package/dist/vike/ClientOnly.mjs +4 -14
- package/dist/vike/ClientOnly.mjs.map +1 -1
- package/dist/vike/{+config.js → config/+config.js} +9 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +165 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +165 -95
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/dist/vike/onRenderHtml.cjs.js +59 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -1
- package/dist/vike/onRenderHtml.mjs +60 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -1
- package/dist/vite/plugin.cjs.js +323 -88
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +323 -89
- package/dist/vite/plugin.mjs.map +1 -1
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +166 -103
- package/package.json +98 -14
- package/src/astro/client.ts +27 -16
- package/src/astro/index.d.ts +24 -0
- package/src/astro/index.ts +69 -3
- package/src/astro/server.ts +8 -2
- package/src/collection.ts +6 -93
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +9 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +9 -7
- package/src/cycle/dom/controlledInputModule.ts +23 -6
- package/src/cycle/dom/enrichEventStream.ts +25 -47
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -56
- package/src/cycle/dom/mockDOMSource.ts +21 -1
- package/src/cycle/dom/modules.ts +16 -3
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +19 -3
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +151 -0
- package/src/devtools.ts +34 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devtools.ts +101 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/collections.ts +1 -1
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +20 -11
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +17 -2
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +163 -0
- package/src/extra/diagnostics/checks/index.ts +97 -4
- package/src/extra/diagnostics/checks/inspect.ts +140 -32
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +10 -5
- package/src/extra/diagnostics/checks/public.d.ts +98 -7
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
- package/src/extra/diagnostics/checks/shared.ts +86 -2
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +100 -7
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +10 -67
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +66 -10
- package/src/extra/diagnostics/codes.ts +268 -23
- package/src/extra/diagnostics/index.ts +34 -9
- package/src/extra/diagnostics/legacy.ts +17 -0
- package/src/extra/driverFactories.ts +83 -19
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +268 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +1 -1
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +90 -217
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +2433 -175
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/index.d.ts +2631 -117
- package/src/index.ts +25 -4
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +256 -133
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +48 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -121
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +7 -1
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/plugin.d.ts +22 -3
- package/src/vite/plugin.ts +222 -24
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/vike/+config.cjs.js +0 -66
- package/dist/vike/+config.cjs.js.map +0 -1
- package/dist/vike/+config.js.map +0 -1
- package/src/component.ts +0 -2301
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -184
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -201
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- package/src/pragma/fn.ts +0 -55
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-4 3-E (GS-10): "Copy as test". Turns a recorded session (./devtoolsActions getSession())
|
|
3
|
+
* into a Vitest + renderComponent test that replays it:
|
|
4
|
+
*
|
|
5
|
+
* renderComponent(C, { initialState?, drivers? }) the session's initial state (left out when
|
|
6
|
+
* it is the component's own initialState)
|
|
7
|
+
* await t.ready()
|
|
8
|
+
* t.simulateAction(type, data) each action caused by 'intent' | 'simulateAction' | 'behavior'
|
|
9
|
+
* await t.respond(sink, body, OK) a reply action from a makeFetchDriver source (renderComponent's
|
|
10
|
+
* await t.fail(sink, status, ...) HTTP fake answers the request the replayed actions send again)
|
|
11
|
+
* await t.settle()
|
|
12
|
+
* expect(t.state).toEqual(final)
|
|
13
|
+
*
|
|
14
|
+
* 'next' and 'built-in' actions are consequences (the replay produces them again), never replayed;
|
|
15
|
+
* nor is an intent action whose data is the component's own state (an intent over STATE.stream).
|
|
16
|
+
* A t.respond / t.fail that follows simulateAction calls waits for the request they send (G-218).
|
|
17
|
+
* Data is written as JS literals: JSON values, Date, Map, Set, NaN / Infinity / -0, bigint. A DOM
|
|
18
|
+
* event or element becomes a stub ({ type, key, target: { dataset, value, checked, id } }), with a
|
|
19
|
+
* comment. Anything else (functions, class instances, cycles) is left out with a comment.
|
|
20
|
+
*
|
|
21
|
+
* The final-state assertion is written only when the replay is complete: every state-changing
|
|
22
|
+
* action could be replayed with its data, no descendant instance changed the state on its own
|
|
23
|
+
* (a child's intent: copy the test from that instance instead), the session wasn't truncated,
|
|
24
|
+
* and the final state is JSON-safe. Otherwise a comment says why. A test written with the
|
|
25
|
+
* assertion passes unchanged (for drivers the session's app had, pass `drivers`).
|
|
26
|
+
*/
|
|
27
|
+
import type {SessionRecording, SessionAction} from './devtoolsActions'
|
|
28
|
+
import {isElement, isEvent, deepEqual} from './devtoolsActions'
|
|
29
|
+
|
|
30
|
+
export interface CopyAsTestOptions {
|
|
31
|
+
/** the import line(s) for the component, e.g. "import App from './App.jsx'" (default: `import <Name> from './<Name>.js'`) */
|
|
32
|
+
componentImport?: string
|
|
33
|
+
/** the identifier the component has in the test (default: the recorded component name) */
|
|
34
|
+
componentName?: string
|
|
35
|
+
/** more import lines (drivers, helpers) */
|
|
36
|
+
imports?: string[]
|
|
37
|
+
/** drivers for renderComponent, as source code by sink name: { DND: 'mockDragDriver().driver' } */
|
|
38
|
+
drivers?: Record<string, string>
|
|
39
|
+
/** more renderComponent options, as source code: "strict: true" */
|
|
40
|
+
renderOptions?: string
|
|
41
|
+
/** the test's name (default: '<Component> session (copied from sygnal/devtools)') */
|
|
42
|
+
testName?: string
|
|
43
|
+
/** adds a `// @vitest-environment <env>` first line (e.g. 'jsdom') */
|
|
44
|
+
environment?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CopyAsTestResult {
|
|
48
|
+
/** the test file */
|
|
49
|
+
code: string
|
|
50
|
+
/** true when the test asserts the final state */
|
|
51
|
+
complete: boolean
|
|
52
|
+
/** what was left out, and why (also comments in the code) */
|
|
53
|
+
warnings: string[]
|
|
54
|
+
/** actions written as simulateAction / respond / fail */
|
|
55
|
+
replayed: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const REPLAY = new Set(['intent', 'simulateAction', 'behavior'])
|
|
59
|
+
const IDENT = /^[A-Za-z_$][\w$]*$/
|
|
60
|
+
|
|
61
|
+
class Unserializable extends Error {}
|
|
62
|
+
|
|
63
|
+
/** a single-quoted JS string literal */
|
|
64
|
+
const str = (s: string) => `'${JSON.stringify(s).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'")}'`
|
|
65
|
+
|
|
66
|
+
interface Lit { code: string; stubbed: boolean }
|
|
67
|
+
|
|
68
|
+
/** `v` as a JS literal. mode 'data' turns DOM events / elements into stubs; 'state' refuses them. */
|
|
69
|
+
export function literal(v: any, mode: 'data' | 'state' = 'data', indent = ''): Lit {
|
|
70
|
+
const flag = {stubbed: false}
|
|
71
|
+
const code = lit(v, mode, indent, new Set(), flag)
|
|
72
|
+
return {code, stubbed: flag.stubbed}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function elementStub(el: any): any {
|
|
76
|
+
const out: any = {}
|
|
77
|
+
const ds = el.dataset && typeof el.dataset == 'object' ? {...el.dataset} : {}
|
|
78
|
+
if (Object.keys(ds).length) out.dataset = ds
|
|
79
|
+
if (el.id) out.id = el.id
|
|
80
|
+
const tag = String(el.tagName).toUpperCase()
|
|
81
|
+
if (/^(INPUT|SELECT|TEXTAREA)$/.test(tag) && typeof el.value == 'string') out.value = el.value
|
|
82
|
+
if (tag == 'INPUT' && /^(checkbox|radio)$/i.test(el.type || '')) out.checked = !!el.checked
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function eventStub(e: any): any {
|
|
87
|
+
const out: any = {type: e.type}
|
|
88
|
+
if (typeof e.key == 'string') out.key = e.key
|
|
89
|
+
if (isElement(e.target)) {
|
|
90
|
+
const t = elementStub(e.target)
|
|
91
|
+
if (Object.keys(t).length) out.target = t
|
|
92
|
+
}
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function lit(v: any, mode: 'data' | 'state', ind: string, seen: Set<any>, flag: {stubbed: boolean}): string {
|
|
97
|
+
if (v === null) return 'null'
|
|
98
|
+
switch (typeof v) {
|
|
99
|
+
case 'undefined': return 'undefined'
|
|
100
|
+
case 'boolean': return String(v)
|
|
101
|
+
case 'string': return str(v)
|
|
102
|
+
case 'number': return Object.is(v, -0) ? '-0' : String(v)
|
|
103
|
+
case 'bigint': return `${v}n`
|
|
104
|
+
case 'function': throw new Unserializable(`a function (${v.name || 'anonymous'})`)
|
|
105
|
+
case 'symbol': throw new Unserializable(`a symbol (${String(v)})`)
|
|
106
|
+
}
|
|
107
|
+
if (seen.has(v)) throw new Unserializable('a circular reference')
|
|
108
|
+
if (isEvent(v) || isElement(v)) {
|
|
109
|
+
if (mode == 'state') throw new Unserializable(isEvent(v) ? `a DOM ${v.type} event` : 'a DOM element')
|
|
110
|
+
flag.stubbed = true
|
|
111
|
+
return lit(isEvent(v) ? eventStub(v) : elementStub(v), mode, ind, seen, flag)
|
|
112
|
+
}
|
|
113
|
+
seen.add(v)
|
|
114
|
+
try {
|
|
115
|
+
const inner = ind + ' '
|
|
116
|
+
const list = (items: string[], open: string, close: string) => {
|
|
117
|
+
if (!items.length) return open + close
|
|
118
|
+
const one = open + (open == '{' ? ' ' : '') + items.join(', ') + (open == '{' ? ' ' : '') + close
|
|
119
|
+
if (one.length + ind.length <= 100 && !one.includes('\n')) return one
|
|
120
|
+
return open + '\n' + items.map(s => inner + s).join(',\n') + ',\n' + ind + close
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(v)) return list(Array.from(v, x => lit(x, mode, inner, seen, flag)), '[', ']')
|
|
123
|
+
if (v instanceof Date) return `new Date(${str(isNaN(+v) ? 'Invalid Date' : v.toISOString())})`
|
|
124
|
+
if (v instanceof Map) return `new Map(${list([...v].map(([k, x]) => list([lit(k, mode, inner + ' ', seen, flag), lit(x, mode, inner + ' ', seen, flag)], '[', ']')), '[', ']')})`
|
|
125
|
+
if (v instanceof Set) return `new Set(${list([...v].map(x => lit(x, mode, inner, seen, flag)), '[', ']')})`
|
|
126
|
+
const proto = Object.getPrototypeOf(v)
|
|
127
|
+
// G-265: a vnode of the JSX pragma (its own constructor, flagged $p) is a plain object
|
|
128
|
+
if (proto !== Object.prototype && proto !== null && !v.$p) throw new Unserializable(`a ${(v.constructor && v.constructor.name) || 'class instance'}`)
|
|
129
|
+
const items: string[] = []
|
|
130
|
+
for (const k of Object.keys(v)) {
|
|
131
|
+
if (v[k] === undefined) continue
|
|
132
|
+
items.push(`${IDENT.test(k) ? k : str(k)}: ${lit(v[k], mode, inner, seen, flag)}`)
|
|
133
|
+
}
|
|
134
|
+
return list(items, '{', '}')
|
|
135
|
+
} finally {
|
|
136
|
+
seen.delete(v)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const q = str
|
|
141
|
+
const tryLit = (v: any, mode: 'data' | 'state', ind: string): Lit | {error: string} => {
|
|
142
|
+
try { return literal(v, mode, ind) } catch (e: any) {
|
|
143
|
+
if (e instanceof Unserializable) return {error: e.message}
|
|
144
|
+
throw e
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** an error reply from makeFetchDriver: { error, request, status?, body? } for the request's `error` action */
|
|
149
|
+
const isErrorReply = (a: SessionAction) =>
|
|
150
|
+
a.data && typeof a.data == 'object' && 'error' in a.data && a.data.request && typeof a.data.request == 'object' && a.data.request.error === a.type
|
|
151
|
+
|
|
152
|
+
/** The test for a recorded session (see the module comment), with what it left out. */
|
|
153
|
+
export function sessionToTest(rec: SessionRecording, options: CopyAsTestOptions = {}): CopyAsTestResult {
|
|
154
|
+
const name = options.componentName || (IDENT.test(rec.component) ? rec.component : 'Component')
|
|
155
|
+
const warnings: string[] = []
|
|
156
|
+
const incomplete: string[] = []
|
|
157
|
+
const body: string[] = []
|
|
158
|
+
const I = ' '
|
|
159
|
+
const drivers = options.drivers || {}
|
|
160
|
+
let replayed = 0
|
|
161
|
+
|
|
162
|
+
if (rec.truncated) incomplete.push('the session lost its oldest actions (clear the log, then record a shorter session)')
|
|
163
|
+
|
|
164
|
+
// render options
|
|
165
|
+
const opts: string[] = []
|
|
166
|
+
if (rec.initialState !== undefined && !deepEqual(rec.initialState, rec.definitionInitialState)) {
|
|
167
|
+
const l = tryLit(rec.initialState, 'state', I)
|
|
168
|
+
if ('error' in l) {
|
|
169
|
+
warnings.push(`the initial state holds ${l.error}: rendering with the component's own initialState`)
|
|
170
|
+
incomplete.push('the initial state could not be written')
|
|
171
|
+
} else opts.push(`initialState: ${l.code}`)
|
|
172
|
+
}
|
|
173
|
+
const fakeable = new Set(rec.fakeable)
|
|
174
|
+
const driverNames = [...new Set([...rec.drivers.filter(n => !fakeable.has(n)), ...Object.keys(drivers)])]
|
|
175
|
+
const given = driverNames.filter(n => drivers[n] !== undefined)
|
|
176
|
+
const missing = driverNames.filter(n => drivers[n] === undefined)
|
|
177
|
+
if (given.length) opts.push(`drivers: { ${given.map(n => `${IDENT.test(n) ? n : q(n)}: ${drivers[n]}`).join(', ')} }`)
|
|
178
|
+
if (missing.length) warnings.push(`the app had ${missing.length == 1 ? 'a driver' : 'drivers'} for ${missing.join(', ')}: pass ${missing.length == 1 ? 'it' : 'them'} with copyAsTest(..., { drivers: { ${missing[0]}: '...' } }) if the component reads ${missing.length == 1 ? 'it' : 'them'}`)
|
|
179
|
+
if (options.renderOptions) opts.push(options.renderOptions)
|
|
180
|
+
|
|
181
|
+
const names = rec.actionNames && new Set(rec.actionNames)
|
|
182
|
+
// (G-218: t.respond / t.fail right after queued simulateAction calls wait for the request
|
|
183
|
+
// those calls send, so no settle() is needed before them)
|
|
184
|
+
const answer = (line: string) => body.push(line)
|
|
185
|
+
for (const a of rec.actions) {
|
|
186
|
+
const changes = a.sinks.includes('STATE') || a.sinks.includes('state')
|
|
187
|
+
if (REPLAY.has(a.cause)) {
|
|
188
|
+
// an intent over the component's own STATE stream: the replay produces it again
|
|
189
|
+
if (a.echo) continue
|
|
190
|
+
// an intent action with no model entry does nothing (renderComponent can't send it)
|
|
191
|
+
if (names && !names.has(a.type) && a.cause != 'behavior') continue
|
|
192
|
+
replayed++
|
|
193
|
+
if (a.data === undefined) { body.push(`${I}t.simulateAction(${q(a.type)})`); continue }
|
|
194
|
+
const l = tryLit(a.data, 'data', I)
|
|
195
|
+
if ('error' in l) {
|
|
196
|
+
body.push(`${I}t.simulateAction(${q(a.type)}) // data left out: it held ${l.error}`)
|
|
197
|
+
if (changes) incomplete.push(`${a.type}'s data could not be written (${l.error})`)
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
if (l.stubbed) body.push(`${I}// ${a.type}: DOM event/element data as a stub (type, key, target dataset/value/checked/id)`)
|
|
201
|
+
body.push(`${I}t.simulateAction(${q(a.type)}, ${l.code})`)
|
|
202
|
+
} else if (a.cause == 'reply') {
|
|
203
|
+
const sink = a.replySink
|
|
204
|
+
if (!sink || a.replyKind != 'fetch' || drivers[sink] !== undefined) {
|
|
205
|
+
body.push(`${I}// reply ${a.type}${sink ? ` from ${sink}` : ''}: not replayed (renderComponent fakes makeFetchDriver sources only)`)
|
|
206
|
+
if (changes) incomplete.push(`the ${a.type} reply could not be replayed`)
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if (isErrorReply(a)) {
|
|
210
|
+
const err = a.data.error, status = a.data.status ?? (err && err.status), errBody = a.data.body ?? (err && err.body)
|
|
211
|
+
if (typeof status == 'number') {
|
|
212
|
+
if (errBody === undefined) answer(`${I}await t.fail(${q(sink)}, ${status}, ${q(a.type)})`)
|
|
213
|
+
else {
|
|
214
|
+
const l = tryLit(errBody, 'state', I)
|
|
215
|
+
if ('error' in l) { answer(`${I}await t.fail(${q(sink)}, ${status}, ${q(a.type)}) // body left out: it held ${l.error}`); if (changes) incomplete.push(`the ${a.type} error body could not be written`) }
|
|
216
|
+
else answer(`${I}await t.fail(${q(sink)}, ${status}, { request: (r) => r.error === ${q(a.type)}, body: ${l.code} })`)
|
|
217
|
+
}
|
|
218
|
+
} else if (a.data.issues) {
|
|
219
|
+
body.push(`${I}// reply ${a.type}: a validation failure, not replayed`)
|
|
220
|
+
if (changes) incomplete.push(`the ${a.type} validation failure could not be replayed`)
|
|
221
|
+
continue
|
|
222
|
+
} else {
|
|
223
|
+
answer(`${I}await t.fail(${q(sink)}, new Error(${str(String(err && err.message || err))}), ${q(a.type)})`)
|
|
224
|
+
}
|
|
225
|
+
replayed++
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
const l = tryLit(a.data, 'state', I)
|
|
229
|
+
if ('error' in l) {
|
|
230
|
+
body.push(`${I}// reply ${a.type} from ${sink}: its data held ${l.error}, not replayed`)
|
|
231
|
+
if (changes) incomplete.push(`the ${a.type} reply's data could not be written`)
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
answer(`${I}await t.respond(${q(sink)}, ${l.code}, ${q(a.type)})`)
|
|
235
|
+
replayed++
|
|
236
|
+
} else if (a.type == 'RESOURCE' && a.data && (a.data.status == 'success' || a.data.status == 'error')) {
|
|
237
|
+
body.push(`${I}// RESOURCE ${a.data.name}: ${a.data.status}, not replayed (answer it with t.respond / t.fail)`)
|
|
238
|
+
if (changes) incomplete.push(`resource '${a.data.name}' was answered`)
|
|
239
|
+
}
|
|
240
|
+
// 'next' and 'built-in': consequences, produced again by the replay
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (rec.foreign.length) {
|
|
244
|
+
const list = rec.foreign.slice(0, 5).map(f => `${f.component}#${f.instance} ${f.type}`).join(', ') + (rec.foreign.length > 5 ? ', ...' : '')
|
|
245
|
+
incomplete.push(`child components changed the state through their own actions (${list}): copy the test from that component instance instead`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let assertion: string[]
|
|
249
|
+
if (!incomplete.length) {
|
|
250
|
+
const l = tryLit(rec.finalState, 'state', I)
|
|
251
|
+
if ('error' in l) incomplete.push(`the final state holds ${l.error}`)
|
|
252
|
+
else assertion = [`${I}expect(t.state).toEqual(${l.code})`]
|
|
253
|
+
}
|
|
254
|
+
if (incomplete.length) {
|
|
255
|
+
warnings.push(...incomplete.map(s => 'no final-state assertion: ' + s))
|
|
256
|
+
assertion = [`${I}// No final-state assertion: ${incomplete.join('; ')}`]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const head: string[] = []
|
|
260
|
+
if (options.environment) head.push(`// @vitest-environment ${options.environment}`)
|
|
261
|
+
head.push(`// Copied from a sygnal/devtools session of ${rec.component} (${rec.actions.length} recorded actions, ${replayed} replayed)`)
|
|
262
|
+
for (const w of warnings) head.push(`// NOTE: ${w}`)
|
|
263
|
+
head.push(`import { it, expect } from 'vitest'`, `import { renderComponent } from 'sygnal'`)
|
|
264
|
+
head.push(...(options.imports || []))
|
|
265
|
+
head.push(options.componentImport || `import ${name} from './${name}.js'`)
|
|
266
|
+
|
|
267
|
+
const testName = options.testName || `${rec.component} session (copied from sygnal/devtools)`
|
|
268
|
+
const render = opts.length ? `renderComponent(${name}, {\n${opts.map(o => `${I}${o},`).join('\n')}\n })` : `renderComponent(${name})`
|
|
269
|
+
const code = [
|
|
270
|
+
...head,
|
|
271
|
+
'',
|
|
272
|
+
`it(${q(testName)}, async () => {`,
|
|
273
|
+
` const t = ${render}`,
|
|
274
|
+
' try {',
|
|
275
|
+
`${I}await t.ready()`,
|
|
276
|
+
...body,
|
|
277
|
+
`${I}await t.settle()`,
|
|
278
|
+
...assertion!,
|
|
279
|
+
' } finally {',
|
|
280
|
+
`${I}t.dispose()`,
|
|
281
|
+
' }',
|
|
282
|
+
'})',
|
|
283
|
+
'',
|
|
284
|
+
].join('\n')
|
|
285
|
+
return {code, complete: !incomplete.length, warnings, replayed}
|
|
286
|
+
}
|
package/src/extra/devtools.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import {getDiagnostics} from './diagnostics/index';
|
|
2
2
|
import type {Diagnostic} from './diagnostics/index';
|
|
3
|
+
import {recordActions, clearActions, getActions, onAction, getSession, preview} from './devtoolsActions';
|
|
4
|
+
import type {DevtoolsAction, SessionRecording} from './devtoolsActions';
|
|
5
|
+
import {sessionToTest} from './copyAsTest';
|
|
6
|
+
import type {CopyAsTestOptions, CopyAsTestResult} from './copyAsTest';
|
|
7
|
+
|
|
8
|
+
import {addLayer, devtoolsHooks} from './devtoolsNext';
|
|
3
9
|
|
|
4
10
|
const DEVTOOLS_SOURCE = '__SYGNAL_DEVTOOLS_PAGE__';
|
|
5
11
|
const EXTENSION_SOURCE = '__SYGNAL_DEVTOOLS_EXTENSION__';
|
|
@@ -9,13 +15,10 @@ declare global {
|
|
|
9
15
|
interface Window {
|
|
10
16
|
__SYGNAL_DEVTOOLS__?: SygnalDevTools;
|
|
11
17
|
__SYGNAL_DEVTOOLS_APP__?: {
|
|
12
|
-
|
|
18
|
+
__runtime?: { setState: (id: any, fn: (s: any) => any) => void };
|
|
13
19
|
};
|
|
14
20
|
__SYGNAL_DEVTOOLS_PAGE__?: boolean;
|
|
15
21
|
SYGNAL_DEBUG?: string | false;
|
|
16
|
-
__SYGNAL_HMR_UPDATING?: boolean;
|
|
17
|
-
__SYGNAL_HMR_STATE?: unknown;
|
|
18
|
-
__SYGNAL_HMR_PERSISTED_STATE?: unknown;
|
|
19
22
|
__SYGNAL_HMR_LAST_CAPTURED_STATE?: unknown;
|
|
20
23
|
Cyclejs?: { sinks?: any };
|
|
21
24
|
CyclejsDevTool_startGraphSerializer?: (sinks: any) => void;
|
|
@@ -58,11 +61,16 @@ interface ExtensionMessage {
|
|
|
58
61
|
source?: string;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
class SygnalDevTools {
|
|
64
|
+
export class SygnalDevTools {
|
|
62
65
|
_connected: boolean;
|
|
66
|
+
_initialized = false;
|
|
63
67
|
_components: Map<number, ComponentMeta>;
|
|
64
68
|
_stateHistory: StateHistoryEntry[];
|
|
65
69
|
_maxHistory: number;
|
|
70
|
+
/** PLAN-4 3-E: actions changed since the last ACTIONS post (flushed on a timeout) */
|
|
71
|
+
_dirty = new Set<DevtoolsAction>();
|
|
72
|
+
_flush: any = null;
|
|
73
|
+
_copyOptions: CopyAsTestOptions = {};
|
|
66
74
|
|
|
67
75
|
constructor() {
|
|
68
76
|
this._connected = false;
|
|
@@ -80,11 +88,24 @@ class SygnalDevTools {
|
|
|
80
88
|
return this._connected && typeof window !== 'undefined';
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
/** Publish the bridge on window and listen for the extension. Idempotent (D77: called once, by 'sygnal/devtools'). */
|
|
83
92
|
init(): void {
|
|
84
|
-
if (typeof window === 'undefined') return;
|
|
93
|
+
if (typeof window === 'undefined' || this._initialized) return;
|
|
94
|
+
this._initialized = true;
|
|
85
95
|
|
|
86
96
|
window.__SYGNAL_DEVTOOLS__ = this;
|
|
87
97
|
|
|
98
|
+
// PLAN-4 3-E (GS-10): the action log (recorded from now on, connected or not)
|
|
99
|
+
recordActions();
|
|
100
|
+
// PLAN-4.6 R4: the apps read this bridge's hooks (devtoolsNext.ts)
|
|
101
|
+
addLayer((api: any) => devtoolsHooks(this, api));
|
|
102
|
+
onAction((a, kind) => {
|
|
103
|
+
if (!this.connected) return;
|
|
104
|
+
if (kind == 'reset') { this._dirty.clear(); this._post('ACTIONS_RESET', {actions: []}); return; }
|
|
105
|
+
this._dirty.add(a!);
|
|
106
|
+
if (this._flush === null) this._flush = setTimeout(() => this._flushActions(), 0);
|
|
107
|
+
});
|
|
108
|
+
|
|
88
109
|
window.addEventListener('message', (event: MessageEvent) => {
|
|
89
110
|
if (event.source !== window) return;
|
|
90
111
|
if (event.data?.source === EXTENSION_SOURCE) {
|
|
@@ -99,6 +120,13 @@ class SygnalDevTools {
|
|
|
99
120
|
this._connected = true;
|
|
100
121
|
if (msg.payload?.maxHistory) this._maxHistory = msg.payload.maxHistory;
|
|
101
122
|
this._sendFullTree();
|
|
123
|
+
this._post('ACTIONS_RESET', {actions: getActions().slice(-500).map(a => this._serializeAction(a))});
|
|
124
|
+
break;
|
|
125
|
+
case 'CLEAR_ACTIONS':
|
|
126
|
+
clearActions();
|
|
127
|
+
break;
|
|
128
|
+
case 'COPY_AS_TEST':
|
|
129
|
+
this._post('COPY_AS_TEST_RESULT', this._copyAsTest(msg.payload?.instance));
|
|
102
130
|
break;
|
|
103
131
|
case 'DISCONNECT':
|
|
104
132
|
this._connected = false;
|
|
@@ -130,7 +158,7 @@ class SygnalDevTools {
|
|
|
130
158
|
hasIntent: !!instance.intent,
|
|
131
159
|
hasContext: !!instance.context,
|
|
132
160
|
hasCalculated: !!instance.calculated,
|
|
133
|
-
components:
|
|
161
|
+
components: [],
|
|
134
162
|
parentId: null,
|
|
135
163
|
children: [],
|
|
136
164
|
debug: instance._debug,
|
|
@@ -283,6 +311,42 @@ class SygnalDevTools {
|
|
|
283
311
|
});
|
|
284
312
|
}
|
|
285
313
|
|
|
314
|
+
/** PLAN-4 3-E: post the actions added or changed since the last flush */
|
|
315
|
+
_flushActions(): void {
|
|
316
|
+
this._flush = null;
|
|
317
|
+
if (!this.connected || !this._dirty.size) { this._dirty.clear(); return; }
|
|
318
|
+
const list = [...this._dirty].sort((a, b) => a.seq - b.seq).map(a => this._serializeAction(a));
|
|
319
|
+
this._dirty.clear();
|
|
320
|
+
this._post('ACTIONS', {actions: list});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
_serializeAction(a: DevtoolsAction): any {
|
|
324
|
+
return {
|
|
325
|
+
seq: a.seq, type: a.type, data: preview(a.data), component: a.component, instance: a.instance, parent: a.parent,
|
|
326
|
+
sinks: [...a.sinks], cause: a.cause, at: a.at,
|
|
327
|
+
...('after' in a ? {before: this._safeClone(a.before), after: this._safeClone(a.after)} : {}),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** "Copy as test" for the panel: the session of `instance` (default: the root) */
|
|
332
|
+
_copyAsTest(instance?: string | number): CopyAsTestResult | {error: string} {
|
|
333
|
+
try {
|
|
334
|
+
return sessionToTest(getSession(instance ?? undefined), this._copyOptions);
|
|
335
|
+
} catch (e: any) {
|
|
336
|
+
return {error: String(e?.message || e)};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Defaults for "Copy as test" from the panel (componentImport, drivers, imports, ...) */
|
|
341
|
+
configureCopyAsTest(options: CopyAsTestOptions): void {
|
|
342
|
+
this._copyOptions = {...this._copyOptions, ...options};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The recorded session of an instance (see sygnal/devtools getSession) */
|
|
346
|
+
getSession(target?: any): SessionRecording {
|
|
347
|
+
return getSession(target);
|
|
348
|
+
}
|
|
349
|
+
|
|
286
350
|
_setDebug({componentId, enabled}: {componentId?: number; enabled: boolean}): void {
|
|
287
351
|
if (typeof componentId === 'undefined' || componentId === null) {
|
|
288
352
|
if (typeof window !== 'undefined') window.SYGNAL_DEBUG = enabled ? 'true' : false;
|
|
@@ -294,7 +358,8 @@ class SygnalDevTools {
|
|
|
294
358
|
if (meta && meta._instanceRef) {
|
|
295
359
|
const instance = meta._instanceRef.deref();
|
|
296
360
|
if (instance) {
|
|
297
|
-
|
|
361
|
+
// (through the app's runtime API)
|
|
362
|
+
instance.__api?.setDebug(componentId, enabled);
|
|
298
363
|
meta.debug = enabled;
|
|
299
364
|
this._post('DEBUG_TOGGLED', {componentId, enabled});
|
|
300
365
|
}
|
|
@@ -311,40 +376,27 @@ class SygnalDevTools {
|
|
|
311
376
|
|
|
312
377
|
const newState = this._safeClone(state);
|
|
313
378
|
|
|
314
|
-
//
|
|
379
|
+
// Per-component time travel through the app's runtime API
|
|
315
380
|
const meta = this._components.get(componentId);
|
|
316
381
|
if (meta) {
|
|
317
382
|
const instance = meta._instanceRef?.deref();
|
|
318
383
|
if (!instance) {
|
|
319
384
|
console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
|
|
320
|
-
} else {
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
stateSink.shamefullySendNext(() => ({...newState}));
|
|
326
|
-
this._post('TIME_TRAVEL_APPLIED', {
|
|
327
|
-
componentId,
|
|
328
|
-
componentName,
|
|
329
|
-
state: newState,
|
|
330
|
-
});
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
console.warn(`[Sygnal DevTools] _timeTravel: component #${componentId} (${componentName}) has no STATE sink with shamefullySendNext. sinkName=${stateSinkName}, hasSinks=${!!instance.sinks}, sinkKeys=${instance.sinks ? Object.keys(instance.sinks).join(',') : 'none'}`);
|
|
385
|
+
} else if (instance.__api) {
|
|
386
|
+
// the runtime's setState (a queued action, one patch)
|
|
387
|
+
instance.__api.setState(componentId, () => ({...newState}));
|
|
388
|
+
this._post('TIME_TRAVEL_APPLIED', {componentId, componentName, state: newState});
|
|
389
|
+
return;
|
|
334
390
|
}
|
|
335
391
|
} else {
|
|
336
392
|
console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
|
|
337
393
|
}
|
|
338
394
|
|
|
339
|
-
// Fall back to
|
|
395
|
+
// Fall back to the root's state
|
|
340
396
|
const app = window.__SYGNAL_DEVTOOLS_APP__;
|
|
341
|
-
if (app?.
|
|
342
|
-
app.
|
|
343
|
-
this._post('TIME_TRAVEL_APPLIED', {
|
|
344
|
-
componentId,
|
|
345
|
-
componentName,
|
|
346
|
-
state: newState,
|
|
347
|
-
});
|
|
397
|
+
if (app?.__runtime) {
|
|
398
|
+
app.__runtime.setState('root', () => ({...newState}));
|
|
399
|
+
this._post('TIME_TRAVEL_APPLIED', {componentId, componentName, state: newState});
|
|
348
400
|
} else {
|
|
349
401
|
console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
|
|
350
402
|
}
|
|
@@ -506,9 +558,27 @@ class SygnalDevTools {
|
|
|
506
558
|
|
|
507
559
|
let instance: SygnalDevTools | null = null;
|
|
508
560
|
|
|
561
|
+
/**
|
|
562
|
+
* The bridge singleton of the 'sygnal/devtools' entry. Not part of the core
|
|
563
|
+
* (D77): the core only calls the hooks of window.__SYGNAL_DEVTOOLS__ when it exists.
|
|
564
|
+
*/
|
|
509
565
|
export function getDevTools(): SygnalDevTools {
|
|
510
566
|
if (!instance) instance = new SygnalDevTools();
|
|
511
567
|
return instance;
|
|
512
568
|
}
|
|
513
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Install the bridge (what run() did before 6.0): window.__SYGNAL_DEVTOOLS__ is
|
|
572
|
+
* set and the extension can connect. A bridge already on window (e.g. a second
|
|
573
|
+
* copy of this entry) is kept. Nothing happens outside a browser.
|
|
574
|
+
*/
|
|
575
|
+
export function installDevTools(): SygnalDevTools | undefined {
|
|
576
|
+
if (typeof window === 'undefined') return undefined;
|
|
577
|
+
const existing = window.__SYGNAL_DEVTOOLS__;
|
|
578
|
+
if (existing) return existing;
|
|
579
|
+
const dt = getDevTools();
|
|
580
|
+
dt.init();
|
|
581
|
+
return dt;
|
|
582
|
+
}
|
|
583
|
+
|
|
514
584
|
export default getDevTools;
|