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,50 @@
|
|
|
1
|
+
/** Structural equality (to depth 5): the core's state/props/context diffing and STATE.watch (GS-6) */
|
|
2
|
+
export function objIsEqual(obj1: any, obj2?: any, maxDepth: number = 5, depth: number = 0): boolean {
|
|
3
|
+
// Past maxDepth the values count as different (false): the caller then treats them as changed
|
|
4
|
+
if (depth > maxDepth) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// If both are the same object or are both exactly null or undefined
|
|
9
|
+
if (obj1 === obj2) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// If either is not an object (null, undefined, or primitive), directly compare
|
|
14
|
+
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Special handling for arrays
|
|
19
|
+
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
|
20
|
+
if (obj1.length !== obj2.length) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
for (let i = 0; i < obj1.length; i++) {
|
|
24
|
+
if (!objIsEqual(obj1[i], obj2[i], maxDepth, depth + 1)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get keys of both objects
|
|
32
|
+
const keys1 = Object.keys(obj1);
|
|
33
|
+
|
|
34
|
+
// Check if the number of properties is different
|
|
35
|
+
if (keys1.length !== Object.keys(obj2).length) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Recursively check each property
|
|
40
|
+
for (const key of keys1) {
|
|
41
|
+
if (!(key in obj2)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (!objIsEqual(obj1[key], obj2[key], maxDepth, depth + 1)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return true;
|
|
50
|
+
}
|
package/src/cycle/state/types.ts
CHANGED
|
@@ -9,14 +9,4 @@ export type Lens<T, R> = {
|
|
|
9
9
|
get: Getter<T, R>;
|
|
10
10
|
set: Setter<T, R>;
|
|
11
11
|
};
|
|
12
|
-
export type ItemScopeFn = (key: string) => string | object;
|
|
13
|
-
export type ItemKeyFn<S> = (state: S, index?: number) => string;
|
|
14
|
-
export type ItemFactoryFn<S, So, Si> = (
|
|
15
|
-
state: S,
|
|
16
|
-
index?: number
|
|
17
|
-
) => MainFn<So, Si>;
|
|
18
12
|
export type Scope<T, R> = string | number | Lens<T, R>;
|
|
19
|
-
export type InternalInstances<Si> = {
|
|
20
|
-
dict: Map<string, Si>;
|
|
21
|
-
arr: Array<Si & {_key: string}>;
|
|
22
|
-
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-4.6 (D162): `defineComponent(opts)` builds a component from an options object, for code
|
|
3
|
+
* that makes components from data (generators, wrappers). It returns an ordinary function
|
|
4
|
+
* component: a new function that calls `view` with the one view argument, with the other options
|
|
5
|
+
* assigned as its statics. There is no separate instantiation path: the core reads the statics
|
|
6
|
+
* like any function component's.
|
|
7
|
+
*
|
|
8
|
+
* const Counter = defineComponent({ name: 'Counter', view, intent, model, initialState: { n: 0 } })
|
|
9
|
+
*
|
|
10
|
+
* `name` becomes `componentName` (diagnostics, devtools, uid()); without it, the view's
|
|
11
|
+
* componentName or function name (G-343: not the `view` an inline `view: () => …` is given;
|
|
12
|
+
* 'Component' then). Statics already on the view are copied, and the options override them. The
|
|
13
|
+
* view function itself is not changed, so one view can back several definitions; the dev checks
|
|
14
|
+
* read it from `__sygnalView` (its arity: positional views).
|
|
15
|
+
*/
|
|
16
|
+
export function defineComponent(opts: Record<string, any>): any {
|
|
17
|
+
if (!opts || typeof opts !== 'object') {
|
|
18
|
+
throw new TypeError('defineComponent(opts): pass an options object, e.g. defineComponent({ view, model, initialState })')
|
|
19
|
+
}
|
|
20
|
+
const {view, name, ...statics} = opts
|
|
21
|
+
if (typeof view !== 'function') {
|
|
22
|
+
throw new TypeError(`defineComponent(${name ? `{ name: '${name}' }` : 'opts'}): view must be a function (the view: ({ state, context, ...props }) => vnode)`)
|
|
23
|
+
}
|
|
24
|
+
const C: any = (args: any) => view(args)
|
|
25
|
+
for (const k of Object.keys(view)) C[k] = view[k]
|
|
26
|
+
const componentName = name || view.componentName || (view.name && view.name != 'view' ? view.name : 'Component')
|
|
27
|
+
C.componentName = componentName
|
|
28
|
+
try { Object.defineProperty(C, 'name', {value: componentName, configurable: true}) } catch (_) { /* name not configurable */ }
|
|
29
|
+
for (const k of Object.keys(statics)) if (statics[k] !== undefined) C[k] = statics[k]
|
|
30
|
+
Object.defineProperty(C, '__sygnalView', {value: view.__sygnalView || view, configurable: true})
|
|
31
|
+
return C
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default defineComponent
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// 'sygnal/devtools' (D77): importing it installs the DevTools bridge
|
|
2
|
+
// (window.__SYGNAL_DEVTOOLS__) in a browser, for the Sygnal DevTools extension.
|
|
3
|
+
// Dev only: sygnal/vite injects it in dev; without Vite, import it before run().
|
|
4
|
+
// Self-contained (like the other dev entries' types); the same shape as
|
|
5
|
+
// SygnalDevTools in 'sygnal'.
|
|
6
|
+
// PLAN-4 3-E (GS-10): it also records the action log and writes "Copy as test" tests.
|
|
7
|
+
|
|
8
|
+
export interface SygnalDevTools {
|
|
9
|
+
/** true while the browser extension is connected */
|
|
10
|
+
readonly connected: boolean
|
|
11
|
+
/** Diagnostics collected so far (same as getDiagnostics() from 'sygnal') */
|
|
12
|
+
getDiagnostics(): Array<{ code: string; severity: string; message: string; [key: string]: any }>
|
|
13
|
+
/** The app graph; present only when the 'sygnal/diagnostics' dev entry is loaded */
|
|
14
|
+
inspect?(): any
|
|
15
|
+
/** PLAN-4 3-E: defaults for "Copy as test" from the extension panel (componentImport, drivers, ...) */
|
|
16
|
+
configureCopyAsTest(options: CopyAsTestOptions): void
|
|
17
|
+
/** PLAN-4 3-E: one instance's recorded session (as getSession()) */
|
|
18
|
+
getSession(target?: Exclude<SessionTarget, SessionRecording>): SessionRecording
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The bridge singleton of this entry (also window.__SYGNAL_DEVTOOLS__ once installed). */
|
|
22
|
+
export declare function getDevTools(): SygnalDevTools
|
|
23
|
+
/** Install the bridge (done on import; idempotent). Undefined outside a browser. */
|
|
24
|
+
export declare function installDevTools(): SygnalDevTools | undefined
|
|
25
|
+
|
|
26
|
+
// ── PLAN-4 3-E (GS-10): the action log, "Copy as test", the Redux DevTools bridge ──
|
|
27
|
+
|
|
28
|
+
export type DevtoolsActionCause = 'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior'
|
|
29
|
+
|
|
30
|
+
/** One recorded action (the entry records every component instance's actions) */
|
|
31
|
+
export interface DevtoolsAction {
|
|
32
|
+
/** Sequence number (session-wide) */
|
|
33
|
+
seq: number
|
|
34
|
+
/** The action name (a behavior's actions are namespaced, `pager.NEXT`) */
|
|
35
|
+
type: string
|
|
36
|
+
/** Its data, as the model got it (a DOM event for a DOM intent stream) */
|
|
37
|
+
data: any
|
|
38
|
+
/** The component's name */
|
|
39
|
+
component: string
|
|
40
|
+
/** The instance's id */
|
|
41
|
+
instance: string
|
|
42
|
+
/** The parent instance's id (null: a root) */
|
|
43
|
+
parent: string | null
|
|
44
|
+
/** Sinks that produced a value for it (live: STATE fills in when its reducer runs) */
|
|
45
|
+
sinks: string[]
|
|
46
|
+
cause: DevtoolsActionCause
|
|
47
|
+
/** ms since the session started */
|
|
48
|
+
at: number
|
|
49
|
+
/** The instance's state before / after the action's STATE reducer, when it changed the state */
|
|
50
|
+
before?: any
|
|
51
|
+
after?: any
|
|
52
|
+
/** A reply action (or RESOURCE): the source that delivered it; 'fetch' when it is makeFetchDriver's */
|
|
53
|
+
replySink?: string
|
|
54
|
+
replyKind?: 'fetch' | 'other'
|
|
55
|
+
/** An intent action whose data is the instance's own state (an intent over STATE.stream) */
|
|
56
|
+
echo?: true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ActionFilter {
|
|
60
|
+
component?: string
|
|
61
|
+
instance?: string | number
|
|
62
|
+
type?: string | RegExp
|
|
63
|
+
cause?: DevtoolsActionCause | DevtoolsActionCause[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One instance's recorded session, as plain data (what copyAsTest() writes the test from) */
|
|
67
|
+
export interface SessionRecording {
|
|
68
|
+
version: 1
|
|
69
|
+
component: string
|
|
70
|
+
instance: string
|
|
71
|
+
/** The state when the session started for this instance */
|
|
72
|
+
initialState?: any
|
|
73
|
+
/** The component's own initialState (renderComponent's default) */
|
|
74
|
+
definitionInitialState?: any
|
|
75
|
+
finalState: any
|
|
76
|
+
/** The action names the component can be sent (model keys, behavior actions) */
|
|
77
|
+
actionNames?: string[]
|
|
78
|
+
/** Source names beyond DOM / EVENTS / STATE / LOG / CHILD / PARENT / READY */
|
|
79
|
+
drivers: string[]
|
|
80
|
+
/** Those of `drivers` renderComponent fakes (makeFetchDriver sources) */
|
|
81
|
+
fakeable: string[]
|
|
82
|
+
/** The instance's own actions, in order */
|
|
83
|
+
actions: Array<Pick<DevtoolsAction, 'type' | 'data' | 'cause' | 'sinks' | 'at' | 'replySink' | 'replyKind' | 'echo'>>
|
|
84
|
+
/** State changes in descendant instances a replay at this instance can't reproduce */
|
|
85
|
+
foreign: Array<{ type: string; component: string; instance: string; cause: DevtoolsActionCause }>
|
|
86
|
+
truncated?: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface CopyAsTestOptions {
|
|
90
|
+
/** The import line(s) for the component (default: `import <Name> from './<Name>.js'`) */
|
|
91
|
+
componentImport?: string
|
|
92
|
+
/** The component's identifier in the test (default: its recorded name) */
|
|
93
|
+
componentName?: string
|
|
94
|
+
/** More import lines (drivers, helpers) */
|
|
95
|
+
imports?: string[]
|
|
96
|
+
/** Drivers for renderComponent, as source code by sink name: { DND: 'mockDragDriver().driver' } */
|
|
97
|
+
drivers?: Record<string, string>
|
|
98
|
+
/** More renderComponent options, as source code: 'strict: true' */
|
|
99
|
+
renderOptions?: string
|
|
100
|
+
/** The test's name */
|
|
101
|
+
testName?: string
|
|
102
|
+
/** Adds a `// @vitest-environment <env>` first line */
|
|
103
|
+
environment?: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface CopyAsTestResult {
|
|
107
|
+
/** The test file */
|
|
108
|
+
code: string
|
|
109
|
+
/** true when the test asserts the final state */
|
|
110
|
+
complete: boolean
|
|
111
|
+
/** What was left out and why (also comments in the code) */
|
|
112
|
+
warnings: string[]
|
|
113
|
+
/** Actions written as simulateAction / t.respond / t.fail */
|
|
114
|
+
replayed: number
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* What a session is taken from: undefined (the newest root), an instance id, a component, run()'s
|
|
119
|
+
* result, or a SessionRecording from getSession()
|
|
120
|
+
*/
|
|
121
|
+
export type SessionTarget = undefined | string | number | ((...args: any[]) => any) | { sources: any } | SessionRecording
|
|
122
|
+
|
|
123
|
+
/** Start recording (done on import in a browser; idempotent). Returns a function that stops it. */
|
|
124
|
+
export declare function recordActions(): () => void
|
|
125
|
+
/** true while the action log records */
|
|
126
|
+
export declare function isRecording(): boolean
|
|
127
|
+
/** Start a new session: forget the recorded actions */
|
|
128
|
+
export declare function clearActions(): void
|
|
129
|
+
/** The session's actions, oldest first, optionally filtered */
|
|
130
|
+
export declare function getActions(filter?: ActionFilter): DevtoolsAction[]
|
|
131
|
+
/** Subscribe to the log; returns unsubscribe */
|
|
132
|
+
export declare function onAction(fn: (action: DevtoolsAction | null, kind: 'add' | 'update' | 'reset') => void): () => void
|
|
133
|
+
/** One instance's session as plain data */
|
|
134
|
+
export declare function getSession(target?: Exclude<SessionTarget, SessionRecording>): SessionRecording
|
|
135
|
+
/** "Copy as test": a Vitest + renderComponent test that replays the session */
|
|
136
|
+
export declare function copyAsTest(target?: SessionTarget, options?: CopyAsTestOptions): string
|
|
137
|
+
/** "Copy as test", with what it left out */
|
|
138
|
+
export declare function copyAsTestResult(target?: SessionTarget, options?: CopyAsTestOptions): CopyAsTestResult
|
|
139
|
+
|
|
140
|
+
export interface ReduxDevtoolsOptions {
|
|
141
|
+
/** The instance name in the extension (default 'Sygnal') */
|
|
142
|
+
name?: string
|
|
143
|
+
/** Which actions to send (default: all but 'built-in' ones) */
|
|
144
|
+
filter?: (action: DevtoolsAction) => boolean
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Send the recorded actions and the root's state to the Redux DevTools extension
|
|
148
|
+
* (window.__REDUX_DEVTOOLS_EXTENSION__); jump to state / action replaces the root's state.
|
|
149
|
+
* Returns a disconnect function (a no-op without the extension).
|
|
150
|
+
*/
|
|
151
|
+
export declare function connectReduxDevtools(target?: Exclude<SessionTarget, SessionRecording>, options?: ReduxDevtoolsOptions): () => void
|
package/src/devtools.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// 'sygnal/devtools' (D77): the DevTools bridge for the browser extension, a dev-only
|
|
2
|
+
// entry. Importing it installs window.__SYGNAL_DEVTOOLS__ (run() did this before 6.0),
|
|
3
|
+
// so import it before run(): components created earlier are not in the tree.
|
|
4
|
+
// sygnal/vite injects it in dev (`vite`, not `vite build`); opt out with
|
|
5
|
+
// sygnal({ devtools: false }). Production builds never contain it.
|
|
6
|
+
// PLAN-4 3-E (GS-10): it also records the action log (in a browser on import; elsewhere
|
|
7
|
+
// after recordActions()), and turns a session into a renderComponent test (copyAsTest).
|
|
8
|
+
import {installDevTools} from './extra/devtools'
|
|
9
|
+
import {getSession, isRecording} from './extra/devtoolsActions'
|
|
10
|
+
import type {SessionRecording} from './extra/devtoolsActions'
|
|
11
|
+
import {sessionToTest} from './extra/copyAsTest'
|
|
12
|
+
import type {CopyAsTestOptions, CopyAsTestResult} from './extra/copyAsTest'
|
|
13
|
+
|
|
14
|
+
installDevTools()
|
|
15
|
+
|
|
16
|
+
export {getDevTools, installDevTools} from './extra/devtools'
|
|
17
|
+
export {recordActions, clearActions, getActions, onAction, getSession, isRecording} from './extra/devtoolsActions'
|
|
18
|
+
export {connectReduxDevtools} from './extra/reduxDevtools'
|
|
19
|
+
|
|
20
|
+
const isSession = (x: any): x is SessionRecording => !!x && typeof x == 'object' && x.version === 1 && Array.isArray(x.actions)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* "Copy as test", with what was left out: the session of `target` (undefined: the newest root;
|
|
24
|
+
* an instance id; a component; run()'s result; or a SessionRecording from getSession()).
|
|
25
|
+
*/
|
|
26
|
+
export function copyAsTestResult(target?: any, options?: CopyAsTestOptions): CopyAsTestResult {
|
|
27
|
+
if (!isSession(target) && !isRecording()) throw new Error('[Sygnal DevTools] copyAsTest: nothing was recorded: import sygnal/devtools before run() (in a browser), or call recordActions() first')
|
|
28
|
+
return sessionToTest(isSession(target) ? target : getSession(target), options)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** "Copy as test": a Vitest + renderComponent test replaying the recorded session (see copyAsTestResult). */
|
|
32
|
+
export function copyAsTest(target?: any, options?: CopyAsTestOptions): string {
|
|
33
|
+
return copyAsTestResult(target, options).code
|
|
34
|
+
}
|
package/src/element.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// 'sygnal/element' (PLAN-4 GS-13, D127): publish a Sygnal component as a custom element.
|
|
2
|
+
// Self-contained (like the other entries' types).
|
|
3
|
+
|
|
4
|
+
export type ElementPropType =
|
|
5
|
+
| StringConstructor
|
|
6
|
+
| NumberConstructor
|
|
7
|
+
| BooleanConstructor
|
|
8
|
+
| ObjectConstructor
|
|
9
|
+
| ArrayConstructor
|
|
10
|
+
|
|
11
|
+
/** The property type a prop declaration gives (attributes are parsed to it). */
|
|
12
|
+
export type ElementPropValue<T> =
|
|
13
|
+
T extends BooleanConstructor ? boolean :
|
|
14
|
+
T extends NumberConstructor ? number | undefined :
|
|
15
|
+
T extends StringConstructor ? string | undefined :
|
|
16
|
+
T extends ArrayConstructor ? any[] | undefined :
|
|
17
|
+
T extends ObjectConstructor ? Record<string, any> | undefined :
|
|
18
|
+
never
|
|
19
|
+
|
|
20
|
+
/** The element's prop properties for a `props` option (an array means all String). */
|
|
21
|
+
export type ElementProps<P> =
|
|
22
|
+
P extends readonly (infer K extends string)[] ? { [N in K]: string | undefined } :
|
|
23
|
+
P extends Record<string, ElementPropType> ? { -readonly [N in keyof P]: ElementPropValue<P[N]> } :
|
|
24
|
+
{}
|
|
25
|
+
|
|
26
|
+
export interface ElementOptions<P extends readonly string[] | Record<string, ElementPropType> = readonly string[] | Record<string, ElementPropType>> {
|
|
27
|
+
/**
|
|
28
|
+
* Properties fed into state: camelCase names; each also has a kebab-case attribute
|
|
29
|
+
* (`dueDate` ↔ `due-date`). String/Number attributes are parsed, Boolean is presence,
|
|
30
|
+
* Object/Array are JSON. An array of names means all String.
|
|
31
|
+
*/
|
|
32
|
+
props?: P
|
|
33
|
+
/**
|
|
34
|
+
* Sink name → CustomEvent name, e.g. `{ PARENT: 'task-picked' }`. The event bubbles and is
|
|
35
|
+
* composed; `detail` is the sink's value. Use lowercase names (React 19 listens with
|
|
36
|
+
* `on<exact-name>`, e.g. `ontask-picked`). DOM, STATE and EVENTS can't be used.
|
|
37
|
+
*/
|
|
38
|
+
events?: Record<string, string>
|
|
39
|
+
/** Render into a shadow root (true = 'open') */
|
|
40
|
+
shadow?: boolean | 'open' | 'closed'
|
|
41
|
+
/** Adopted into the shadow root, shared by every instance (ignored without shadow) */
|
|
42
|
+
styles?: string | CSSStyleSheet | Array<string | CSSStyleSheet>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SygnalElementConstructor<P = {}> {
|
|
46
|
+
new (): HTMLElement & ElementProps<P>
|
|
47
|
+
prototype: HTMLElement & ElementProps<P>
|
|
48
|
+
readonly observedAttributes: string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Define `tag` as a custom element that runs `Component` (a plain function component) as
|
|
53
|
+
* its own app. Define it before a host framework renders the tag, so props arrive as
|
|
54
|
+
* properties. Calling it again for the same tag hot-swaps the component in every live
|
|
55
|
+
* element, keeping each one's state (HMR), and returns the same constructor.
|
|
56
|
+
*/
|
|
57
|
+
export declare function defineElement<
|
|
58
|
+
const P extends readonly string[] | Record<string, ElementPropType> = {}
|
|
59
|
+
>(tag: string, Component: (...args: any[]) => any, options?: ElementOptions<P>): SygnalElementConstructor<P>
|
package/src/element.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// 'sygnal/element' (PLAN-4 GS-13, D127): publish a Sygnal component as a custom element.
|
|
2
|
+
//
|
|
3
|
+
// defineElement('task-board', TaskBoard, {
|
|
4
|
+
// props: { title: String, tasks: Array, readonly: Boolean }, // or ['a', 'b'] (all String)
|
|
5
|
+
// events: { PARENT: 'task-picked' }, // sink name → CustomEvent name
|
|
6
|
+
// shadow: true, // true | 'open' | 'closed'
|
|
7
|
+
// styles: '.title { color: green }', // string | CSSStyleSheet | array
|
|
8
|
+
// })
|
|
9
|
+
//
|
|
10
|
+
// Each connected element is its own app: run(Component) mounted on a div inside the element
|
|
11
|
+
// (or its shadow root). Props are in state from the first render (the component's
|
|
12
|
+
// initialState, overlaid with the props); later property/attribute changes are merged into
|
|
13
|
+
// state. Sinks listed in `events` are dispatched as CustomEvents (bubbles + composed; the
|
|
14
|
+
// PARENT wrapper is unwrapped to its value). Removing the element disposes the app; a move
|
|
15
|
+
// (disconnect + connect in the same task) keeps it running. Calling defineElement again for
|
|
16
|
+
// the same tag (HMR) swaps the component in every live element, keeping each one's state.
|
|
17
|
+
//
|
|
18
|
+
// Separate entry: rollup rewrites './index' to the external 'sygnal' (0 B in the core bundle).
|
|
19
|
+
import {run} from './index';
|
|
20
|
+
import {owned} from './extra/owned';
|
|
21
|
+
|
|
22
|
+
export type ElementPropType =
|
|
23
|
+
| StringConstructor
|
|
24
|
+
| NumberConstructor
|
|
25
|
+
| BooleanConstructor
|
|
26
|
+
| ObjectConstructor
|
|
27
|
+
| ArrayConstructor;
|
|
28
|
+
|
|
29
|
+
export interface ElementOptions {
|
|
30
|
+
/** Properties fed into state (camelCase; the attribute is kebab-case). An array means all String. */
|
|
31
|
+
props?: readonly string[] | Record<string, ElementPropType>;
|
|
32
|
+
/** Sink name → CustomEvent name, e.g. { PARENT: 'task-picked' } */
|
|
33
|
+
events?: Record<string, string>;
|
|
34
|
+
/** Render into a shadow root (true = 'open') */
|
|
35
|
+
shadow?: boolean | 'open' | 'closed';
|
|
36
|
+
/** Adopted into the shadow root, shared by every instance (ignored without shadow) */
|
|
37
|
+
styles?: string | CSSStyleSheet | Array<string | CSSStyleSheet>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Sinks that are the app's own drivers, never events
|
|
41
|
+
const RESERVED_SINKS = ['DOM', 'STATE', 'EVENTS'];
|
|
42
|
+
|
|
43
|
+
// Hot-swap functions of the tags this module defined
|
|
44
|
+
const swaps = new WeakMap<CustomElementConstructor, (Component: any) => void>();
|
|
45
|
+
|
|
46
|
+
const kebab = (name: string) => name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
|
47
|
+
|
|
48
|
+
function parseAttribute(type: ElementPropType, value: string | null): any {
|
|
49
|
+
if (type === Boolean) return value !== null;
|
|
50
|
+
if (value === null) return undefined;
|
|
51
|
+
if (type === Number) return Number(value);
|
|
52
|
+
if (type === String) return value;
|
|
53
|
+
return JSON.parse(value); // Object, Array (throws on bad JSON)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toSheet(style: string | CSSStyleSheet): CSSStyleSheet {
|
|
57
|
+
if (typeof style !== 'string') return style;
|
|
58
|
+
const sheet = new CSSStyleSheet();
|
|
59
|
+
sheet.replaceSync(style);
|
|
60
|
+
return sheet;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const devMode = () => (globalThis as any).__SYGNAL_DEV__ === true;
|
|
64
|
+
|
|
65
|
+
export function defineElement(tag: string, Component: any, options: ElementOptions = {}): CustomElementConstructor {
|
|
66
|
+
const prior = customElements.get(tag);
|
|
67
|
+
const swap = prior && swaps.get(prior);
|
|
68
|
+
if (swap) {
|
|
69
|
+
swap(Component); // HMR: the defining module ran again
|
|
70
|
+
return prior!;
|
|
71
|
+
}
|
|
72
|
+
if (typeof Component !== 'function') {
|
|
73
|
+
throw new TypeError(`defineElement('${tag}'): pass a function component`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const types: Record<string, ElementPropType> = Array.isArray(options.props)
|
|
77
|
+
? Object.fromEntries(options.props.map((name: string) => [name, String]))
|
|
78
|
+
: {...(options.props as Record<string, ElementPropType> | undefined)};
|
|
79
|
+
const propOfAttribute: Record<string, string> = Object.fromEntries(Object.keys(types).map((name) => [kebab(name), name]));
|
|
80
|
+
const events = {...options.events};
|
|
81
|
+
for (const sink of RESERVED_SINKS) {
|
|
82
|
+
if (sink in events) throw new TypeError(`defineElement('${tag}'): '${sink}' is a driver of the element's app, not an event sink`);
|
|
83
|
+
}
|
|
84
|
+
const shadowMode = options.shadow ? (options.shadow === 'closed' ? 'closed' : 'open') : undefined;
|
|
85
|
+
const sheets = shadowMode ? ([] as Array<string | CSSStyleSheet>).concat(options.styles || []).map(toSheet) : [];
|
|
86
|
+
// Props that hide a member of HTMLElement (e.g. title, hidden): SYG644 once per tag (D131)
|
|
87
|
+
const shadowedMembers = Object.keys(types).filter((name) => name in HTMLElement.prototype);
|
|
88
|
+
let warned = false;
|
|
89
|
+
|
|
90
|
+
const live = new Set<SygnalElement>();
|
|
91
|
+
let instances = 0;
|
|
92
|
+
|
|
93
|
+
class SygnalElement extends HTMLElement {
|
|
94
|
+
static observedAttributes = Object.keys(propOfAttribute);
|
|
95
|
+
|
|
96
|
+
#props: Record<string, any> = {};
|
|
97
|
+
#app: any = undefined;
|
|
98
|
+
#root: HTMLElement | ShadowRoot = this;
|
|
99
|
+
#uid = `${tag}-${++instances}`;
|
|
100
|
+
|
|
101
|
+
static {
|
|
102
|
+
for (const name of Object.keys(types)) {
|
|
103
|
+
Object.defineProperty(this.prototype, name, {
|
|
104
|
+
configurable: true,
|
|
105
|
+
enumerable: true,
|
|
106
|
+
get(this: SygnalElement) {
|
|
107
|
+
return this.#props[name];
|
|
108
|
+
},
|
|
109
|
+
set(this: SygnalElement, value: any) {
|
|
110
|
+
this.#setProp(name, value);
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
swaps.set(this, (Next: any) => {
|
|
115
|
+
Component = Next;
|
|
116
|
+
live.forEach((element) => element.#swap());
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
constructor() {
|
|
121
|
+
super();
|
|
122
|
+
if (shadowMode) {
|
|
123
|
+
const root = this.attachShadow({mode: shadowMode});
|
|
124
|
+
root.adoptedStyleSheets = sheets;
|
|
125
|
+
this.#root = root;
|
|
126
|
+
}
|
|
127
|
+
// Properties set before the tag was defined are own properties that hide the
|
|
128
|
+
// accessors: move them onto the accessors
|
|
129
|
+
for (const name of Object.keys(types)) {
|
|
130
|
+
if (Object.prototype.hasOwnProperty.call(this, name)) {
|
|
131
|
+
const value = (this as any)[name];
|
|
132
|
+
delete (this as any)[name];
|
|
133
|
+
this.#setProp(name, value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
attributeChangedCallback(attribute: string, _old: string | null, value: string | null) {
|
|
139
|
+
const name = propOfAttribute[attribute];
|
|
140
|
+
try {
|
|
141
|
+
this.#setProp(name, parseAttribute(types[name], value));
|
|
142
|
+
} catch (_) {
|
|
143
|
+
// bad JSON in an Object/Array attribute: keep the previous value
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
connectedCallback() {
|
|
148
|
+
if (this.#app) return; // moved, not removed: keep running
|
|
149
|
+
// (G-216: a hot swap elsewhere on the page is that app's own, so no need to wait it out)
|
|
150
|
+
if (shadowedMembers.length && !warned) {
|
|
151
|
+
// D131: SYG644 through the diagnostics core when diagnostics are on; else a console
|
|
152
|
+
// warning in dev
|
|
153
|
+
const message = `props hide the HTMLElement members of the same name (${shadowedMembers.join(', ')})`;
|
|
154
|
+
const fix = 'Rename them to keep the native behaviour';
|
|
155
|
+
let reported: any;
|
|
156
|
+
try {
|
|
157
|
+
reported = (globalThis as any).__SYGNAL_DIAGNOSTICS__?.report('SYG644', {
|
|
158
|
+
severity: 'warn', component: `<${tag}>`, message, fix, data: {tag, members: shadowedMembers},
|
|
159
|
+
});
|
|
160
|
+
} catch (e) {
|
|
161
|
+
reported = true; // mode 'error': thrown after the element starts
|
|
162
|
+
queueMicrotask(() => { throw e; });
|
|
163
|
+
}
|
|
164
|
+
if (reported || devMode()) warned = true;
|
|
165
|
+
if (!reported && devMode()) console.warn(`[sygnal/element] <${tag}>: ${message}. ${fix}.`);
|
|
166
|
+
}
|
|
167
|
+
const mount = document.createElement('div');
|
|
168
|
+
this.#root.appendChild(mount);
|
|
169
|
+
this.#app = run(this.#component(), this.#eventDrivers(), {mountPoint: mount as any, uid: this.#uid});
|
|
170
|
+
this.#releaseDevtools();
|
|
171
|
+
live.add(this);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
disconnectedCallback() {
|
|
175
|
+
// Deferred: a move is a disconnect + connect in the same task
|
|
176
|
+
queueMicrotask(() => {
|
|
177
|
+
if (this.isConnected || !this.#app) return;
|
|
178
|
+
this.#app.dispose();
|
|
179
|
+
this.#app = undefined;
|
|
180
|
+
live.delete(this);
|
|
181
|
+
this.#root.replaceChildren();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Hot swap (defineElement again for this tag): the new component, this element's state
|
|
186
|
+
#swap() {
|
|
187
|
+
if (!this.#app) return;
|
|
188
|
+
// (the state through the runtime API, 04 §3.9)
|
|
189
|
+
this.#app.hmr(this.#component(), (this.#app as any).__runtime.getState());
|
|
190
|
+
this.#releaseDevtools();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#setProp(name: string, value: any) {
|
|
194
|
+
this.#props[name] = value;
|
|
195
|
+
const set = (state: any) => ({...state, [name]: value});
|
|
196
|
+
(this.#app as any)?.__runtime?.setState('root', set);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// A per-instance copy of the component whose initialState includes the current props
|
|
200
|
+
// (owned: the dev statics freeze leaves the host's values alone, G-275)
|
|
201
|
+
#component() {
|
|
202
|
+
const wrapped = Object.assign((...args: any[]) => Component(...args), Component, {
|
|
203
|
+
initialState: owned({...Component.initialState, ...this.#props}),
|
|
204
|
+
});
|
|
205
|
+
return Object.defineProperty(wrapped, 'name', {value: Component.name || tag});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#eventDrivers() {
|
|
209
|
+
return Object.fromEntries(
|
|
210
|
+
Object.entries(events).map(([sink, eventName]) => [
|
|
211
|
+
sink,
|
|
212
|
+
(sink$: any) => {
|
|
213
|
+
sink$.addListener({
|
|
214
|
+
next: (value: any) => {
|
|
215
|
+
// PARENT values arrive wrapped as { name, component, value }
|
|
216
|
+
const detail = sink === 'PARENT' ? value?.value : value;
|
|
217
|
+
this.dispatchEvent(new CustomEvent(eventName, {detail, bubbles: true, composed: true}));
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
])
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// run() registers the first live app for DevTools time travel (G-212). An element gives
|
|
226
|
+
// the slot back, so a host app that starts later gets it
|
|
227
|
+
#releaseDevtools() {
|
|
228
|
+
const w = window as any;
|
|
229
|
+
if (w.__SYGNAL_DEVTOOLS_APP__ === this.#app || w.__SYGNAL_DEVTOOLS_APP__?.sinks === this.#app.sinks) w.__SYGNAL_DEVTOOLS_APP__ = undefined;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
customElements.define(tag, SygnalElement);
|
|
234
|
+
return SygnalElement;
|
|
235
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-3: the retry delay shared by makeSocketDriver's `reconnect` and makeFetchDriver's `retry`.
|
|
3
|
+
* Delay of retry n (from 0): `min(maxDelayMs, delayMs * 2^n)`, varied by ±`jitter` (a fraction;
|
|
4
|
+
* `false` = none). Defaults `{ delayMs: 500, maxDelayMs: 10000, jitter: 0.2 }`.
|
|
5
|
+
*/
|
|
6
|
+
export const backoff = (p: {delayMs?: number; maxDelayMs?: number; jitter?: number | boolean}, n: number) => {
|
|
7
|
+
const j = p.jitter === false ? 0 : p.jitter == null || p.jitter === true ? 0.2 : +p.jitter;
|
|
8
|
+
return Math.min(p.maxDelayMs ?? 10000, (p.delayMs ?? 500) * 2 ** n) * (1 + j * (2 * Math.random() - 1));
|
|
9
|
+
};
|