sygnal 5.2.0 → 5.3.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/README.md +21 -2
- package/dist/astro/client.cjs.js +271 -14
- package/dist/astro/client.mjs +271 -14
- package/dist/astro/server.cjs.js +18 -0
- package/dist/astro/server.mjs +18 -0
- package/dist/index.cjs.js +543 -17
- package/dist/index.d.ts +63 -0
- package/dist/index.esm.js +538 -18
- package/dist/sygnal.min.js +1 -1
- package/dist/vike/+config.cjs.js +5 -1
- package/dist/vike/+config.js +5 -1
- package/dist/vike/ClientOnly.cjs.js +34 -0
- package/dist/vike/ClientOnly.mjs +32 -0
- package/dist/vike/onRenderClient.cjs.js +292 -35
- package/dist/vike/onRenderClient.mjs +292 -35
- package/dist/vike/onRenderHtml.cjs.js +71 -34
- package/dist/vike/onRenderHtml.mjs +71 -34
- package/dist/vite/plugin.cjs.js +6 -4
- package/dist/vite/plugin.mjs +6 -4
- package/package.json +5 -1
- package/src/component.ts +63 -6
- package/src/extra/command.ts +13 -2
- package/src/extra/devtools.ts +221 -7
- package/src/extra/dragDriver.ts +59 -5
- package/src/extra/eventDriver.ts +6 -2
- package/src/extra/pwa.ts +179 -0
- package/src/extra/reducers.ts +64 -0
- package/src/extra/ssr.ts +19 -0
- package/src/index.d.ts +63 -0
- package/src/index.ts +2 -0
- package/src/vike/+config.ts +5 -1
- package/src/vike/ClientOnly.ts +10 -0
- package/src/vike/onRenderClient.ts +319 -36
- package/src/vike/onRenderHtml.ts +77 -33
- package/src/vike/types.ts +2 -0
- package/src/vite/plugin.ts +6 -4
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ cd my-app
|
|
|
26
26
|
npm run dev
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Choose from Vite (SPA), Vike (SSR), or Astro templates in JavaScript or TypeScript.
|
|
29
|
+
Choose from Vite (SPA), Vite + PWA, Vike (SSR), or Astro templates in JavaScript or TypeScript.
|
|
30
30
|
|
|
31
31
|
**Or add to an existing project:**
|
|
32
32
|
|
|
@@ -325,6 +325,25 @@ MyComponent.model = {
|
|
|
325
325
|
|
|
326
326
|
For advanced cases needing stream composition, the `dispose$` source is also available in intent.
|
|
327
327
|
|
|
328
|
+
### PWA Helpers
|
|
329
|
+
|
|
330
|
+
Built-in service worker driver, online/offline detection, and install prompt handling:
|
|
331
|
+
|
|
332
|
+
```jsx
|
|
333
|
+
import { run, makeServiceWorkerDriver, onlineStatus$, createInstallPrompt } from 'sygnal'
|
|
334
|
+
|
|
335
|
+
const installPrompt = createInstallPrompt()
|
|
336
|
+
|
|
337
|
+
run(App, { SW: makeServiceWorkerDriver('/sw.js') })
|
|
338
|
+
|
|
339
|
+
App.intent = ({ DOM, SW }) => ({
|
|
340
|
+
ONLINE_CHANGED: onlineStatus$(),
|
|
341
|
+
UPDATE_READY: SW.select('waiting'),
|
|
342
|
+
APPLY_UPDATE: DOM.click('.update-btn'),
|
|
343
|
+
INSTALL: DOM.click('.install-btn'),
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
328
347
|
### Testing
|
|
329
348
|
|
|
330
349
|
Test components in isolation with `renderComponent`:
|
|
@@ -404,7 +423,7 @@ import vikeSygnal from 'sygnal/config'
|
|
|
404
423
|
export default { extends: [vikeSygnal] }
|
|
405
424
|
```
|
|
406
425
|
|
|
407
|
-
Pages are standard Sygnal components in `pages/*/+Page.jsx`. Supports layouts, data fetching, and
|
|
426
|
+
Pages are standard Sygnal components in `pages/*/+Page.jsx`. Supports layouts, data fetching, SPA mode, and `ClientOnly` for browser-only components.
|
|
408
427
|
|
|
409
428
|
### TypeScript
|
|
410
429
|
|
package/dist/astro/client.cjs.js
CHANGED
|
@@ -5029,7 +5029,12 @@ function makeDOMDriver(container, options = {}) {
|
|
|
5029
5029
|
function eventBusDriver(out$) {
|
|
5030
5030
|
const events = new EventTarget();
|
|
5031
5031
|
out$.subscribe({
|
|
5032
|
-
next: (event) =>
|
|
5032
|
+
next: (event) => {
|
|
5033
|
+
events.dispatchEvent(new CustomEvent('data', { detail: event }));
|
|
5034
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
5035
|
+
window.__SYGNAL_DEVTOOLS__.onBusEvent(event);
|
|
5036
|
+
}
|
|
5037
|
+
},
|
|
5033
5038
|
error: (err) => console.error('[EVENTS driver] Error in sink stream:', err),
|
|
5034
5039
|
});
|
|
5035
5040
|
return {
|
|
@@ -5532,7 +5537,7 @@ function component(opts) {
|
|
|
5532
5537
|
return returnFunction;
|
|
5533
5538
|
}
|
|
5534
5539
|
class Component {
|
|
5535
|
-
constructor({ name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', onError, debug = false }) {
|
|
5540
|
+
constructor({ name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', isolatedState = false, onError, debug = false }) {
|
|
5536
5541
|
if (!sources || !isObj(sources))
|
|
5537
5542
|
throw new Error(`[${name}] Missing or invalid sources`);
|
|
5538
5543
|
this._componentNumber = COMPONENT_COUNT++;
|
|
@@ -5554,6 +5559,7 @@ class Component {
|
|
|
5554
5559
|
this.requestSourceName = requestSourceName;
|
|
5555
5560
|
this.sourceNames = Object.keys(sources);
|
|
5556
5561
|
this.onError = onError;
|
|
5562
|
+
this.isolatedState = isolatedState;
|
|
5557
5563
|
this._debug = debug;
|
|
5558
5564
|
// Warn if calculated fields shadow base state keys
|
|
5559
5565
|
if (this.calculated && this.initialState
|
|
@@ -5682,6 +5688,9 @@ class Component {
|
|
|
5682
5688
|
this.sources.props$ = props$.map((val) => {
|
|
5683
5689
|
const { sygnalFactory, sygnalOptions, ...sanitizedProps } = val;
|
|
5684
5690
|
this.currentProps = sanitizedProps;
|
|
5691
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
5692
|
+
window.__SYGNAL_DEVTOOLS__.onPropsChanged(this._componentNumber, this.name, sanitizedProps);
|
|
5693
|
+
}
|
|
5685
5694
|
return val;
|
|
5686
5695
|
});
|
|
5687
5696
|
}
|
|
@@ -5753,6 +5762,9 @@ class Component {
|
|
|
5753
5762
|
}
|
|
5754
5763
|
}
|
|
5755
5764
|
dispose() {
|
|
5765
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
5766
|
+
window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name);
|
|
5767
|
+
}
|
|
5756
5768
|
// Fire the DISPOSE built-in action so model handlers can run cleanup logic
|
|
5757
5769
|
const hasDispose = this.model && (this.model[DISPOSE_ACTION] || Object.keys(this.model).some(k => k.includes('|') && k.split('|')[0].trim() === DISPOSE_ACTION));
|
|
5758
5770
|
if (hasDispose && this.action$ && typeof this.action$.shamefullySendNext === 'function') {
|
|
@@ -5953,7 +5965,7 @@ class Component {
|
|
|
5953
5965
|
const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE;
|
|
5954
5966
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
|
|
5955
5967
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
|
|
5956
|
-
if (this.isSubComponent && this.initialState) {
|
|
5968
|
+
if (this.isSubComponent && this.initialState && !this.isolatedState) {
|
|
5957
5969
|
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
|
|
5958
5970
|
}
|
|
5959
5971
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined');
|
|
@@ -6134,6 +6146,7 @@ class Component {
|
|
|
6134
6146
|
.map((vdom) => processLazy(vdom, this))
|
|
6135
6147
|
.map(processPortals)
|
|
6136
6148
|
.map(processTransitions)
|
|
6149
|
+
.map(processClientOnly)
|
|
6137
6150
|
.compose(this.instantiateSubComponents.bind(this))
|
|
6138
6151
|
.filter((val) => val !== undefined)
|
|
6139
6152
|
.compose(this.renderVdom.bind(this));
|
|
@@ -6149,6 +6162,12 @@ class Component {
|
|
|
6149
6162
|
else {
|
|
6150
6163
|
acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []));
|
|
6151
6164
|
}
|
|
6165
|
+
// Stamp EVENTS sink emissions with emitter component info for devtools
|
|
6166
|
+
if (name === 'EVENTS' && acc[name]) {
|
|
6167
|
+
const _componentNumber = this._componentNumber;
|
|
6168
|
+
const _name = this.name;
|
|
6169
|
+
acc[name] = acc[name].map((ev) => ({ ...ev, __emitterId: _componentNumber, __emitterName: _name }));
|
|
6170
|
+
}
|
|
6152
6171
|
return acc;
|
|
6153
6172
|
}, {});
|
|
6154
6173
|
this.sinks[this.DOMSourceName] = this.vdom$;
|
|
@@ -6671,6 +6690,13 @@ class Component {
|
|
|
6671
6690
|
if (!isObj(sink$)) {
|
|
6672
6691
|
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`);
|
|
6673
6692
|
}
|
|
6693
|
+
// Notify devtools of collection mount
|
|
6694
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
6695
|
+
const itemName = typeof collectionOf === 'function'
|
|
6696
|
+
? (collectionOf.componentName || collectionOf.label || collectionOf.name || 'anonymous')
|
|
6697
|
+
: String(collectionOf);
|
|
6698
|
+
window.__SYGNAL_DEVTOOLS__.onCollectionMounted(this._componentNumber, this.name, itemName, typeof stateField === 'string' ? stateField : null);
|
|
6699
|
+
}
|
|
6674
6700
|
return sink$;
|
|
6675
6701
|
}
|
|
6676
6702
|
instantiateSwitchable(el, props$, children$) {
|
|
@@ -6809,6 +6835,7 @@ class Component {
|
|
|
6809
6835
|
for (const key of Object.keys(props)) {
|
|
6810
6836
|
const val = props[key];
|
|
6811
6837
|
if (val && val.__sygnalCommand) {
|
|
6838
|
+
val._targetComponentName = componentName;
|
|
6812
6839
|
sources.commands$ = makeCommandSource(val);
|
|
6813
6840
|
break;
|
|
6814
6841
|
}
|
|
@@ -6851,10 +6878,15 @@ class Component {
|
|
|
6851
6878
|
const wasReady = this._childReadyState[id];
|
|
6852
6879
|
this._childReadyState[id] = !!ready;
|
|
6853
6880
|
// When READY state changes, trigger a re-render
|
|
6854
|
-
if (wasReady !== !!ready
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6881
|
+
if (wasReady !== !!ready) {
|
|
6882
|
+
if (this._readyChangedListener) {
|
|
6883
|
+
setTimeout(() => {
|
|
6884
|
+
this._readyChangedListener?.next(null);
|
|
6885
|
+
}, 0);
|
|
6886
|
+
}
|
|
6887
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
6888
|
+
window.__SYGNAL_DEVTOOLS__.onReadyChanged(this._componentNumber, this.name, id, !!ready);
|
|
6889
|
+
}
|
|
6858
6890
|
}
|
|
6859
6891
|
},
|
|
6860
6892
|
error: () => { },
|
|
@@ -7111,6 +7143,31 @@ function processTransitions(vnode) {
|
|
|
7111
7143
|
}
|
|
7112
7144
|
return vnode;
|
|
7113
7145
|
}
|
|
7146
|
+
function processClientOnly(vnode) {
|
|
7147
|
+
if (!vnode || !vnode.sel)
|
|
7148
|
+
return vnode;
|
|
7149
|
+
if (vnode.sel === 'clientonly') {
|
|
7150
|
+
// On the client, unwrap to children (render them normally)
|
|
7151
|
+
const children = vnode.children || [];
|
|
7152
|
+
if (children.length === 0)
|
|
7153
|
+
return { sel: 'div', data: {}, children: [] };
|
|
7154
|
+
if (children.length === 1)
|
|
7155
|
+
return processClientOnly(children[0]);
|
|
7156
|
+
// Multiple children: wrap in a div
|
|
7157
|
+
return {
|
|
7158
|
+
sel: 'div',
|
|
7159
|
+
data: {},
|
|
7160
|
+
children: children.map(processClientOnly),
|
|
7161
|
+
text: undefined,
|
|
7162
|
+
elm: undefined,
|
|
7163
|
+
key: undefined,
|
|
7164
|
+
};
|
|
7165
|
+
}
|
|
7166
|
+
if (vnode.children && vnode.children.length > 0) {
|
|
7167
|
+
vnode.children = vnode.children.map(processClientOnly);
|
|
7168
|
+
}
|
|
7169
|
+
return vnode;
|
|
7170
|
+
}
|
|
7114
7171
|
function applyTransitionHooks(vnode, name, duration) {
|
|
7115
7172
|
const existingInsert = vnode.data?.hook?.insert;
|
|
7116
7173
|
const existingRemove = vnode.data?.hook?.remove;
|
|
@@ -7465,6 +7522,12 @@ class SygnalDevTools {
|
|
|
7465
7522
|
case 'TIME_TRAVEL':
|
|
7466
7523
|
this._timeTravel(msg.payload);
|
|
7467
7524
|
break;
|
|
7525
|
+
case 'SNAPSHOT':
|
|
7526
|
+
this._takeSnapshot();
|
|
7527
|
+
break;
|
|
7528
|
+
case 'RESTORE_SNAPSHOT':
|
|
7529
|
+
this._restoreSnapshot(msg.payload);
|
|
7530
|
+
break;
|
|
7468
7531
|
case 'GET_STATE':
|
|
7469
7532
|
this._sendComponentState(msg.payload.componentId);
|
|
7470
7533
|
break;
|
|
@@ -7484,6 +7547,7 @@ class SygnalDevTools {
|
|
|
7484
7547
|
children: [],
|
|
7485
7548
|
debug: instance._debug,
|
|
7486
7549
|
createdAt: Date.now(),
|
|
7550
|
+
mviGraph: this._extractMviGraph(instance),
|
|
7487
7551
|
_instanceRef: new WeakRef(instance),
|
|
7488
7552
|
};
|
|
7489
7553
|
this._components.set(componentNumber, meta);
|
|
@@ -7508,7 +7572,6 @@ class SygnalDevTools {
|
|
|
7508
7572
|
componentId: componentNumber,
|
|
7509
7573
|
componentName: name,
|
|
7510
7574
|
state: entry.state,
|
|
7511
|
-
historyIndex: this._stateHistory.length - 1,
|
|
7512
7575
|
});
|
|
7513
7576
|
}
|
|
7514
7577
|
onActionDispatched(componentNumber, name, actionType, data) {
|
|
@@ -7545,6 +7608,76 @@ class SygnalDevTools {
|
|
|
7545
7608
|
componentId: componentNumber,
|
|
7546
7609
|
componentName: name,
|
|
7547
7610
|
context: this._safeClone(context),
|
|
7611
|
+
contextTrace: this._buildContextTrace(componentNumber, context),
|
|
7612
|
+
});
|
|
7613
|
+
}
|
|
7614
|
+
onPropsChanged(componentNumber, name, props) {
|
|
7615
|
+
if (!this.connected)
|
|
7616
|
+
return;
|
|
7617
|
+
this._post('PROPS_CHANGED', {
|
|
7618
|
+
componentId: componentNumber,
|
|
7619
|
+
componentName: name,
|
|
7620
|
+
props: this._safeClone(props),
|
|
7621
|
+
});
|
|
7622
|
+
}
|
|
7623
|
+
onBusEvent(event) {
|
|
7624
|
+
if (!this.connected)
|
|
7625
|
+
return;
|
|
7626
|
+
this._post('BUS_EVENT', {
|
|
7627
|
+
type: event.type,
|
|
7628
|
+
data: this._safeClone(event.data),
|
|
7629
|
+
componentId: event.__emitterId ?? null,
|
|
7630
|
+
componentName: event.__emitterName ?? null,
|
|
7631
|
+
timestamp: Date.now(),
|
|
7632
|
+
});
|
|
7633
|
+
}
|
|
7634
|
+
onCommandSent(type, data, targetComponentId, targetComponentName) {
|
|
7635
|
+
if (!this.connected)
|
|
7636
|
+
return;
|
|
7637
|
+
this._post('COMMAND_SENT', {
|
|
7638
|
+
type,
|
|
7639
|
+
data: this._safeClone(data),
|
|
7640
|
+
targetComponentName: targetComponentName ?? null,
|
|
7641
|
+
timestamp: Date.now(),
|
|
7642
|
+
});
|
|
7643
|
+
}
|
|
7644
|
+
onReadyChanged(parentId, parentName, childKey, ready) {
|
|
7645
|
+
if (!this.connected)
|
|
7646
|
+
return;
|
|
7647
|
+
this._post('READY_CHANGED', {
|
|
7648
|
+
parentId,
|
|
7649
|
+
parentName,
|
|
7650
|
+
childKey,
|
|
7651
|
+
ready,
|
|
7652
|
+
timestamp: Date.now(),
|
|
7653
|
+
});
|
|
7654
|
+
}
|
|
7655
|
+
onCollectionMounted(parentId, parentName, itemComponentName, stateField) {
|
|
7656
|
+
const meta = this._components.get(parentId);
|
|
7657
|
+
if (meta) {
|
|
7658
|
+
meta.collection = { itemComponent: itemComponentName, stateField };
|
|
7659
|
+
}
|
|
7660
|
+
if (!this.connected)
|
|
7661
|
+
return;
|
|
7662
|
+
this._post('COLLECTION_MOUNTED', {
|
|
7663
|
+
parentId,
|
|
7664
|
+
parentName,
|
|
7665
|
+
itemComponent: itemComponentName,
|
|
7666
|
+
stateField,
|
|
7667
|
+
});
|
|
7668
|
+
}
|
|
7669
|
+
onComponentDisposed(componentNumber, name) {
|
|
7670
|
+
const meta = this._components.get(componentNumber);
|
|
7671
|
+
if (meta) {
|
|
7672
|
+
meta.disposed = true;
|
|
7673
|
+
meta.disposedAt = Date.now();
|
|
7674
|
+
}
|
|
7675
|
+
if (!this.connected)
|
|
7676
|
+
return;
|
|
7677
|
+
this._post('COMPONENT_DISPOSED', {
|
|
7678
|
+
componentId: componentNumber,
|
|
7679
|
+
componentName: name,
|
|
7680
|
+
timestamp: Date.now(),
|
|
7548
7681
|
});
|
|
7549
7682
|
}
|
|
7550
7683
|
onDebugLog(componentNumber, message) {
|
|
@@ -7573,20 +7706,79 @@ class SygnalDevTools {
|
|
|
7573
7706
|
}
|
|
7574
7707
|
}
|
|
7575
7708
|
}
|
|
7576
|
-
_timeTravel({
|
|
7577
|
-
|
|
7578
|
-
|
|
7709
|
+
_timeTravel({ componentId, componentName, state }) {
|
|
7710
|
+
if (componentId == null || !state) {
|
|
7711
|
+
console.warn('[Sygnal DevTools] _timeTravel: missing componentId or state', { componentId, hasState: !!state });
|
|
7579
7712
|
return;
|
|
7713
|
+
}
|
|
7580
7714
|
if (typeof window === 'undefined')
|
|
7581
7715
|
return;
|
|
7716
|
+
const newState = this._safeClone(state);
|
|
7717
|
+
// Try per-component time-travel via the component's STATE sink (reducer stream)
|
|
7718
|
+
const meta = this._components.get(componentId);
|
|
7719
|
+
if (meta) {
|
|
7720
|
+
const instance = meta._instanceRef?.deref();
|
|
7721
|
+
if (!instance) {
|
|
7722
|
+
console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
|
|
7723
|
+
}
|
|
7724
|
+
else {
|
|
7725
|
+
// sinks[stateSourceName] is the reducer stream — push a reducer that replaces state
|
|
7726
|
+
const stateSinkName = instance.stateSourceName || 'STATE';
|
|
7727
|
+
const stateSink = instance.sinks?.[stateSinkName];
|
|
7728
|
+
if (stateSink?.shamefullySendNext) {
|
|
7729
|
+
stateSink.shamefullySendNext(() => ({ ...newState }));
|
|
7730
|
+
this._post('TIME_TRAVEL_APPLIED', {
|
|
7731
|
+
componentId,
|
|
7732
|
+
componentName,
|
|
7733
|
+
state: newState,
|
|
7734
|
+
});
|
|
7735
|
+
return;
|
|
7736
|
+
}
|
|
7737
|
+
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'}`);
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
else {
|
|
7741
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
|
|
7742
|
+
}
|
|
7743
|
+
// Fall back to root STATE sink for root-level components
|
|
7582
7744
|
const app = window.__SYGNAL_DEVTOOLS_APP__;
|
|
7583
7745
|
if (app?.sinks?.STATE?.shamefullySendNext) {
|
|
7584
|
-
app.sinks.STATE.shamefullySendNext(() => ({ ...
|
|
7746
|
+
app.sinks.STATE.shamefullySendNext(() => ({ ...newState }));
|
|
7585
7747
|
this._post('TIME_TRAVEL_APPLIED', {
|
|
7586
|
-
|
|
7587
|
-
|
|
7748
|
+
componentId,
|
|
7749
|
+
componentName,
|
|
7750
|
+
state: newState,
|
|
7588
7751
|
});
|
|
7589
7752
|
}
|
|
7753
|
+
else {
|
|
7754
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7757
|
+
_takeSnapshot() {
|
|
7758
|
+
const entries = [];
|
|
7759
|
+
for (const [id, meta] of this._components) {
|
|
7760
|
+
if (meta.disposed)
|
|
7761
|
+
continue;
|
|
7762
|
+
const instance = meta._instanceRef?.deref();
|
|
7763
|
+
if (instance?.currentState != null) {
|
|
7764
|
+
entries.push({
|
|
7765
|
+
componentId: id,
|
|
7766
|
+
componentName: meta.name,
|
|
7767
|
+
state: this._safeClone(instance.currentState),
|
|
7768
|
+
});
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
this._post('SNAPSHOT_TAKEN', {
|
|
7772
|
+
entries,
|
|
7773
|
+
timestamp: Date.now(),
|
|
7774
|
+
});
|
|
7775
|
+
}
|
|
7776
|
+
_restoreSnapshot(snapshot) {
|
|
7777
|
+
if (!snapshot?.entries)
|
|
7778
|
+
return;
|
|
7779
|
+
for (const entry of snapshot.entries) {
|
|
7780
|
+
this._timeTravel(entry);
|
|
7781
|
+
}
|
|
7590
7782
|
}
|
|
7591
7783
|
_sendComponentState(componentId) {
|
|
7592
7784
|
const meta = this._components.get(componentId);
|
|
@@ -7597,11 +7789,38 @@ class SygnalDevTools {
|
|
|
7597
7789
|
componentId,
|
|
7598
7790
|
state: this._safeClone(instance.currentState),
|
|
7599
7791
|
context: this._safeClone(instance.currentContext),
|
|
7792
|
+
contextTrace: this._buildContextTrace(componentId, instance.currentContext),
|
|
7600
7793
|
props: this._safeClone(instance.currentProps),
|
|
7601
7794
|
});
|
|
7602
7795
|
}
|
|
7603
7796
|
}
|
|
7604
7797
|
}
|
|
7798
|
+
_buildContextTrace(componentId, context) {
|
|
7799
|
+
if (!context || typeof context !== 'object')
|
|
7800
|
+
return [];
|
|
7801
|
+
const trace = [];
|
|
7802
|
+
const fields = Object.keys(context);
|
|
7803
|
+
for (const field of fields) {
|
|
7804
|
+
// Walk up parent chain to find which component provides this field
|
|
7805
|
+
let currentId = componentId;
|
|
7806
|
+
let found = false;
|
|
7807
|
+
while (currentId != null) {
|
|
7808
|
+
const meta = this._components.get(currentId);
|
|
7809
|
+
if (!meta)
|
|
7810
|
+
break;
|
|
7811
|
+
if (meta.mviGraph?.contextProvides?.includes(field)) {
|
|
7812
|
+
trace.push({ field, providerId: meta.id, providerName: meta.name });
|
|
7813
|
+
found = true;
|
|
7814
|
+
break;
|
|
7815
|
+
}
|
|
7816
|
+
currentId = meta.parentId;
|
|
7817
|
+
}
|
|
7818
|
+
if (!found) {
|
|
7819
|
+
trace.push({ field, providerId: -1, providerName: 'unknown' });
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
7822
|
+
return trace;
|
|
7823
|
+
}
|
|
7605
7824
|
_sendFullTree() {
|
|
7606
7825
|
const tree = [];
|
|
7607
7826
|
for (const [, meta] of this._components) {
|
|
@@ -7636,6 +7855,44 @@ class SygnalDevTools {
|
|
|
7636
7855
|
return '[unserializable]';
|
|
7637
7856
|
}
|
|
7638
7857
|
}
|
|
7858
|
+
_extractMviGraph(instance) {
|
|
7859
|
+
if (!instance.model)
|
|
7860
|
+
return null;
|
|
7861
|
+
try {
|
|
7862
|
+
const sources = instance.sourceNames ? [...instance.sourceNames] : [];
|
|
7863
|
+
const actions = [];
|
|
7864
|
+
const model = instance.model;
|
|
7865
|
+
for (const key of Object.keys(model)) {
|
|
7866
|
+
let actionName = key;
|
|
7867
|
+
let entry = model[key];
|
|
7868
|
+
// Handle shorthand 'ACTION | SINK'
|
|
7869
|
+
if (key.includes('|')) {
|
|
7870
|
+
const parts = key.split('|').map((s) => s.trim());
|
|
7871
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
7872
|
+
actionName = parts[0];
|
|
7873
|
+
actions.push({ name: actionName, sinks: [parts[1]] });
|
|
7874
|
+
continue;
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
// Function → targets STATE
|
|
7878
|
+
if (typeof entry === 'function') {
|
|
7879
|
+
actions.push({ name: actionName, sinks: [instance.stateSourceName || 'STATE'] });
|
|
7880
|
+
continue;
|
|
7881
|
+
}
|
|
7882
|
+
// Object → keys are sink names
|
|
7883
|
+
if (entry && typeof entry === 'object') {
|
|
7884
|
+
actions.push({ name: actionName, sinks: Object.keys(entry) });
|
|
7885
|
+
continue;
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
const contextProvides = instance.context && typeof instance.context === 'object'
|
|
7889
|
+
? Object.keys(instance.context) : [];
|
|
7890
|
+
return { sources, actions, contextProvides };
|
|
7891
|
+
}
|
|
7892
|
+
catch (e) {
|
|
7893
|
+
return null;
|
|
7894
|
+
}
|
|
7895
|
+
}
|
|
7639
7896
|
_serializeMeta(meta) {
|
|
7640
7897
|
const { _instanceRef, ...rest } = meta;
|
|
7641
7898
|
return rest;
|