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/dist/index.esm.js
CHANGED
|
@@ -2146,11 +2146,17 @@ function createCommand() {
|
|
|
2146
2146
|
start(l) { listener.next = (val) => l.next(val); },
|
|
2147
2147
|
stop() { listener.next = () => { }; },
|
|
2148
2148
|
});
|
|
2149
|
-
|
|
2150
|
-
send: (type, data) =>
|
|
2149
|
+
const cmd = {
|
|
2150
|
+
send: (type, data) => {
|
|
2151
|
+
listener.next({ type, data });
|
|
2152
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
2153
|
+
window.__SYGNAL_DEVTOOLS__.onCommandSent(type, data, cmd._targetComponentId, cmd._targetComponentName);
|
|
2154
|
+
}
|
|
2155
|
+
},
|
|
2151
2156
|
_stream,
|
|
2152
2157
|
__sygnalCommand: true,
|
|
2153
2158
|
};
|
|
2159
|
+
return cmd;
|
|
2154
2160
|
}
|
|
2155
2161
|
function makeCommandSource(cmd) {
|
|
2156
2162
|
return {
|
|
@@ -2253,7 +2259,7 @@ function component(opts) {
|
|
|
2253
2259
|
return returnFunction;
|
|
2254
2260
|
}
|
|
2255
2261
|
class Component {
|
|
2256
|
-
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 }) {
|
|
2262
|
+
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 }) {
|
|
2257
2263
|
if (!sources || !isObj(sources))
|
|
2258
2264
|
throw new Error(`[${name}] Missing or invalid sources`);
|
|
2259
2265
|
this._componentNumber = COMPONENT_COUNT++;
|
|
@@ -2275,6 +2281,7 @@ class Component {
|
|
|
2275
2281
|
this.requestSourceName = requestSourceName;
|
|
2276
2282
|
this.sourceNames = Object.keys(sources);
|
|
2277
2283
|
this.onError = onError;
|
|
2284
|
+
this.isolatedState = isolatedState;
|
|
2278
2285
|
this._debug = debug;
|
|
2279
2286
|
// Warn if calculated fields shadow base state keys
|
|
2280
2287
|
if (this.calculated && this.initialState
|
|
@@ -2403,6 +2410,9 @@ class Component {
|
|
|
2403
2410
|
this.sources.props$ = props$.map((val) => {
|
|
2404
2411
|
const { sygnalFactory, sygnalOptions, ...sanitizedProps } = val;
|
|
2405
2412
|
this.currentProps = sanitizedProps;
|
|
2413
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
2414
|
+
window.__SYGNAL_DEVTOOLS__.onPropsChanged(this._componentNumber, this.name, sanitizedProps);
|
|
2415
|
+
}
|
|
2406
2416
|
return val;
|
|
2407
2417
|
});
|
|
2408
2418
|
}
|
|
@@ -2474,6 +2484,9 @@ class Component {
|
|
|
2474
2484
|
}
|
|
2475
2485
|
}
|
|
2476
2486
|
dispose() {
|
|
2487
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
2488
|
+
window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name);
|
|
2489
|
+
}
|
|
2477
2490
|
// Fire the DISPOSE built-in action so model handlers can run cleanup logic
|
|
2478
2491
|
const hasDispose = this.model && (this.model[DISPOSE_ACTION] || Object.keys(this.model).some(k => k.includes('|') && k.split('|')[0].trim() === DISPOSE_ACTION));
|
|
2479
2492
|
if (hasDispose && this.action$ && typeof this.action$.shamefullySendNext === 'function') {
|
|
@@ -2674,7 +2687,7 @@ class Component {
|
|
|
2674
2687
|
const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE;
|
|
2675
2688
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
|
|
2676
2689
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
|
|
2677
|
-
if (this.isSubComponent && this.initialState) {
|
|
2690
|
+
if (this.isSubComponent && this.initialState && !this.isolatedState) {
|
|
2678
2691
|
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
|
|
2679
2692
|
}
|
|
2680
2693
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined');
|
|
@@ -2855,6 +2868,7 @@ class Component {
|
|
|
2855
2868
|
.map((vdom) => processLazy(vdom, this))
|
|
2856
2869
|
.map(processPortals)
|
|
2857
2870
|
.map(processTransitions)
|
|
2871
|
+
.map(processClientOnly)
|
|
2858
2872
|
.compose(this.instantiateSubComponents.bind(this))
|
|
2859
2873
|
.filter((val) => val !== undefined)
|
|
2860
2874
|
.compose(this.renderVdom.bind(this));
|
|
@@ -2870,6 +2884,12 @@ class Component {
|
|
|
2870
2884
|
else {
|
|
2871
2885
|
acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []));
|
|
2872
2886
|
}
|
|
2887
|
+
// Stamp EVENTS sink emissions with emitter component info for devtools
|
|
2888
|
+
if (name === 'EVENTS' && acc[name]) {
|
|
2889
|
+
const _componentNumber = this._componentNumber;
|
|
2890
|
+
const _name = this.name;
|
|
2891
|
+
acc[name] = acc[name].map((ev) => ({ ...ev, __emitterId: _componentNumber, __emitterName: _name }));
|
|
2892
|
+
}
|
|
2873
2893
|
return acc;
|
|
2874
2894
|
}, {});
|
|
2875
2895
|
this.sinks[this.DOMSourceName] = this.vdom$;
|
|
@@ -3392,6 +3412,13 @@ class Component {
|
|
|
3392
3412
|
if (!isObj(sink$)) {
|
|
3393
3413
|
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`);
|
|
3394
3414
|
}
|
|
3415
|
+
// Notify devtools of collection mount
|
|
3416
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3417
|
+
const itemName = typeof collectionOf === 'function'
|
|
3418
|
+
? (collectionOf.componentName || collectionOf.label || collectionOf.name || 'anonymous')
|
|
3419
|
+
: String(collectionOf);
|
|
3420
|
+
window.__SYGNAL_DEVTOOLS__.onCollectionMounted(this._componentNumber, this.name, itemName, typeof stateField === 'string' ? stateField : null);
|
|
3421
|
+
}
|
|
3395
3422
|
return sink$;
|
|
3396
3423
|
}
|
|
3397
3424
|
instantiateSwitchable(el, props$, children$) {
|
|
@@ -3530,6 +3557,7 @@ class Component {
|
|
|
3530
3557
|
for (const key of Object.keys(props)) {
|
|
3531
3558
|
const val = props[key];
|
|
3532
3559
|
if (val && val.__sygnalCommand) {
|
|
3560
|
+
val._targetComponentName = componentName;
|
|
3533
3561
|
sources.commands$ = makeCommandSource(val);
|
|
3534
3562
|
break;
|
|
3535
3563
|
}
|
|
@@ -3572,10 +3600,15 @@ class Component {
|
|
|
3572
3600
|
const wasReady = this._childReadyState[id];
|
|
3573
3601
|
this._childReadyState[id] = !!ready;
|
|
3574
3602
|
// When READY state changes, trigger a re-render
|
|
3575
|
-
if (wasReady !== !!ready
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3603
|
+
if (wasReady !== !!ready) {
|
|
3604
|
+
if (this._readyChangedListener) {
|
|
3605
|
+
setTimeout(() => {
|
|
3606
|
+
this._readyChangedListener?.next(null);
|
|
3607
|
+
}, 0);
|
|
3608
|
+
}
|
|
3609
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3610
|
+
window.__SYGNAL_DEVTOOLS__.onReadyChanged(this._componentNumber, this.name, id, !!ready);
|
|
3611
|
+
}
|
|
3579
3612
|
}
|
|
3580
3613
|
},
|
|
3581
3614
|
error: () => { },
|
|
@@ -3832,6 +3865,31 @@ function processTransitions(vnode) {
|
|
|
3832
3865
|
}
|
|
3833
3866
|
return vnode;
|
|
3834
3867
|
}
|
|
3868
|
+
function processClientOnly(vnode) {
|
|
3869
|
+
if (!vnode || !vnode.sel)
|
|
3870
|
+
return vnode;
|
|
3871
|
+
if (vnode.sel === 'clientonly') {
|
|
3872
|
+
// On the client, unwrap to children (render them normally)
|
|
3873
|
+
const children = vnode.children || [];
|
|
3874
|
+
if (children.length === 0)
|
|
3875
|
+
return { sel: 'div', data: {}, children: [] };
|
|
3876
|
+
if (children.length === 1)
|
|
3877
|
+
return processClientOnly(children[0]);
|
|
3878
|
+
// Multiple children: wrap in a div
|
|
3879
|
+
return {
|
|
3880
|
+
sel: 'div',
|
|
3881
|
+
data: {},
|
|
3882
|
+
children: children.map(processClientOnly),
|
|
3883
|
+
text: undefined,
|
|
3884
|
+
elm: undefined,
|
|
3885
|
+
key: undefined,
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
if (vnode.children && vnode.children.length > 0) {
|
|
3889
|
+
vnode.children = vnode.children.map(processClientOnly);
|
|
3890
|
+
}
|
|
3891
|
+
return vnode;
|
|
3892
|
+
}
|
|
3835
3893
|
function applyTransitionHooks(vnode, name, duration) {
|
|
3836
3894
|
const existingInsert = vnode.data?.hook?.insert;
|
|
3837
3895
|
const existingRemove = vnode.data?.hook?.remove;
|
|
@@ -4391,6 +4449,40 @@ function processDrag({ draggable, dropZone } = {}, options = {}) {
|
|
|
4391
4449
|
return { dragStart$, dragEnd$, dragOver$, drop$ };
|
|
4392
4450
|
}
|
|
4393
4451
|
|
|
4452
|
+
/**
|
|
4453
|
+
* Adds chainable convenience methods to a DND event stream,
|
|
4454
|
+
* mirroring the DOM driver's `enrichEventStream` pattern.
|
|
4455
|
+
*
|
|
4456
|
+
* DND.dragstart('task').data('taskId')
|
|
4457
|
+
* DND.dragstart('task').data('taskId', Number)
|
|
4458
|
+
* DND.drop('lane').data('laneId')
|
|
4459
|
+
* DND.dragstart('task').element()
|
|
4460
|
+
*/
|
|
4461
|
+
function enrichDragStream(stream$) {
|
|
4462
|
+
// .data(name, fn?) — extract dataset[name] from dragstart payload,
|
|
4463
|
+
// or dropZone.dataset[name] from drop payload
|
|
4464
|
+
stream$.data = function data(name, fn) {
|
|
4465
|
+
const mapped = stream$.map((e) => {
|
|
4466
|
+
// dragstart payload: { element, dataset }
|
|
4467
|
+
// drop payload: { dropZone, insertBefore }
|
|
4468
|
+
const val = e?.dataset?.[name]
|
|
4469
|
+
?? e?.dropZone?.dataset?.[name]
|
|
4470
|
+
?? e?.element?.dataset?.[name];
|
|
4471
|
+
return fn ? fn(val) : val;
|
|
4472
|
+
});
|
|
4473
|
+
return enrichDragStream(mapped);
|
|
4474
|
+
};
|
|
4475
|
+
// .element(fn?) — extract the primary element from the payload
|
|
4476
|
+
stream$.element = function element(fn) {
|
|
4477
|
+
const mapped = stream$.map((e) => {
|
|
4478
|
+
const el = e?.element ?? e?.dropZone ?? null;
|
|
4479
|
+
return fn ? fn(el) : el;
|
|
4480
|
+
});
|
|
4481
|
+
return enrichDragStream(mapped);
|
|
4482
|
+
};
|
|
4483
|
+
return stream$;
|
|
4484
|
+
}
|
|
4485
|
+
// ─── Driver Factory ──────────────────────────────────────────────────────────
|
|
4394
4486
|
function makeDragDriver() {
|
|
4395
4487
|
return function dragDriver(sink$) {
|
|
4396
4488
|
const categories = new Map();
|
|
@@ -4482,7 +4574,7 @@ function makeDragDriver() {
|
|
|
4482
4574
|
events(eventType) {
|
|
4483
4575
|
const busEventName = `${category}:${eventType}`;
|
|
4484
4576
|
let handler;
|
|
4485
|
-
|
|
4577
|
+
const stream$ = xs__default.create({
|
|
4486
4578
|
start(listener) {
|
|
4487
4579
|
handler = ({ detail }) => listener.next(detail);
|
|
4488
4580
|
bus.addEventListener(busEventName, handler);
|
|
@@ -4492,6 +4584,7 @@ function makeDragDriver() {
|
|
|
4492
4584
|
bus.removeEventListener(busEventName, handler);
|
|
4493
4585
|
},
|
|
4494
4586
|
});
|
|
4587
|
+
return enrichDragStream(stream$);
|
|
4495
4588
|
},
|
|
4496
4589
|
};
|
|
4497
4590
|
},
|
|
@@ -4671,7 +4764,12 @@ function setupReusable(drivers) {
|
|
|
4671
4764
|
function eventBusDriver(out$) {
|
|
4672
4765
|
const events = new EventTarget();
|
|
4673
4766
|
out$.subscribe({
|
|
4674
|
-
next: (event) =>
|
|
4767
|
+
next: (event) => {
|
|
4768
|
+
events.dispatchEvent(new CustomEvent('data', { detail: event }));
|
|
4769
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
4770
|
+
window.__SYGNAL_DEVTOOLS__.onBusEvent(event);
|
|
4771
|
+
}
|
|
4772
|
+
},
|
|
4675
4773
|
error: (err) => console.error('[EVENTS driver] Error in sink stream:', err),
|
|
4676
4774
|
});
|
|
4677
4775
|
return {
|
|
@@ -4751,6 +4849,12 @@ class SygnalDevTools {
|
|
|
4751
4849
|
case 'TIME_TRAVEL':
|
|
4752
4850
|
this._timeTravel(msg.payload);
|
|
4753
4851
|
break;
|
|
4852
|
+
case 'SNAPSHOT':
|
|
4853
|
+
this._takeSnapshot();
|
|
4854
|
+
break;
|
|
4855
|
+
case 'RESTORE_SNAPSHOT':
|
|
4856
|
+
this._restoreSnapshot(msg.payload);
|
|
4857
|
+
break;
|
|
4754
4858
|
case 'GET_STATE':
|
|
4755
4859
|
this._sendComponentState(msg.payload.componentId);
|
|
4756
4860
|
break;
|
|
@@ -4770,6 +4874,7 @@ class SygnalDevTools {
|
|
|
4770
4874
|
children: [],
|
|
4771
4875
|
debug: instance._debug,
|
|
4772
4876
|
createdAt: Date.now(),
|
|
4877
|
+
mviGraph: this._extractMviGraph(instance),
|
|
4773
4878
|
_instanceRef: new WeakRef(instance),
|
|
4774
4879
|
};
|
|
4775
4880
|
this._components.set(componentNumber, meta);
|
|
@@ -4794,7 +4899,6 @@ class SygnalDevTools {
|
|
|
4794
4899
|
componentId: componentNumber,
|
|
4795
4900
|
componentName: name,
|
|
4796
4901
|
state: entry.state,
|
|
4797
|
-
historyIndex: this._stateHistory.length - 1,
|
|
4798
4902
|
});
|
|
4799
4903
|
}
|
|
4800
4904
|
onActionDispatched(componentNumber, name, actionType, data) {
|
|
@@ -4831,6 +4935,76 @@ class SygnalDevTools {
|
|
|
4831
4935
|
componentId: componentNumber,
|
|
4832
4936
|
componentName: name,
|
|
4833
4937
|
context: this._safeClone(context),
|
|
4938
|
+
contextTrace: this._buildContextTrace(componentNumber, context),
|
|
4939
|
+
});
|
|
4940
|
+
}
|
|
4941
|
+
onPropsChanged(componentNumber, name, props) {
|
|
4942
|
+
if (!this.connected)
|
|
4943
|
+
return;
|
|
4944
|
+
this._post('PROPS_CHANGED', {
|
|
4945
|
+
componentId: componentNumber,
|
|
4946
|
+
componentName: name,
|
|
4947
|
+
props: this._safeClone(props),
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
onBusEvent(event) {
|
|
4951
|
+
if (!this.connected)
|
|
4952
|
+
return;
|
|
4953
|
+
this._post('BUS_EVENT', {
|
|
4954
|
+
type: event.type,
|
|
4955
|
+
data: this._safeClone(event.data),
|
|
4956
|
+
componentId: event.__emitterId ?? null,
|
|
4957
|
+
componentName: event.__emitterName ?? null,
|
|
4958
|
+
timestamp: Date.now(),
|
|
4959
|
+
});
|
|
4960
|
+
}
|
|
4961
|
+
onCommandSent(type, data, targetComponentId, targetComponentName) {
|
|
4962
|
+
if (!this.connected)
|
|
4963
|
+
return;
|
|
4964
|
+
this._post('COMMAND_SENT', {
|
|
4965
|
+
type,
|
|
4966
|
+
data: this._safeClone(data),
|
|
4967
|
+
targetComponentName: targetComponentName ?? null,
|
|
4968
|
+
timestamp: Date.now(),
|
|
4969
|
+
});
|
|
4970
|
+
}
|
|
4971
|
+
onReadyChanged(parentId, parentName, childKey, ready) {
|
|
4972
|
+
if (!this.connected)
|
|
4973
|
+
return;
|
|
4974
|
+
this._post('READY_CHANGED', {
|
|
4975
|
+
parentId,
|
|
4976
|
+
parentName,
|
|
4977
|
+
childKey,
|
|
4978
|
+
ready,
|
|
4979
|
+
timestamp: Date.now(),
|
|
4980
|
+
});
|
|
4981
|
+
}
|
|
4982
|
+
onCollectionMounted(parentId, parentName, itemComponentName, stateField) {
|
|
4983
|
+
const meta = this._components.get(parentId);
|
|
4984
|
+
if (meta) {
|
|
4985
|
+
meta.collection = { itemComponent: itemComponentName, stateField };
|
|
4986
|
+
}
|
|
4987
|
+
if (!this.connected)
|
|
4988
|
+
return;
|
|
4989
|
+
this._post('COLLECTION_MOUNTED', {
|
|
4990
|
+
parentId,
|
|
4991
|
+
parentName,
|
|
4992
|
+
itemComponent: itemComponentName,
|
|
4993
|
+
stateField,
|
|
4994
|
+
});
|
|
4995
|
+
}
|
|
4996
|
+
onComponentDisposed(componentNumber, name) {
|
|
4997
|
+
const meta = this._components.get(componentNumber);
|
|
4998
|
+
if (meta) {
|
|
4999
|
+
meta.disposed = true;
|
|
5000
|
+
meta.disposedAt = Date.now();
|
|
5001
|
+
}
|
|
5002
|
+
if (!this.connected)
|
|
5003
|
+
return;
|
|
5004
|
+
this._post('COMPONENT_DISPOSED', {
|
|
5005
|
+
componentId: componentNumber,
|
|
5006
|
+
componentName: name,
|
|
5007
|
+
timestamp: Date.now(),
|
|
4834
5008
|
});
|
|
4835
5009
|
}
|
|
4836
5010
|
onDebugLog(componentNumber, message) {
|
|
@@ -4859,20 +5033,79 @@ class SygnalDevTools {
|
|
|
4859
5033
|
}
|
|
4860
5034
|
}
|
|
4861
5035
|
}
|
|
4862
|
-
_timeTravel({
|
|
4863
|
-
|
|
4864
|
-
|
|
5036
|
+
_timeTravel({ componentId, componentName, state }) {
|
|
5037
|
+
if (componentId == null || !state) {
|
|
5038
|
+
console.warn('[Sygnal DevTools] _timeTravel: missing componentId or state', { componentId, hasState: !!state });
|
|
4865
5039
|
return;
|
|
5040
|
+
}
|
|
4866
5041
|
if (typeof window === 'undefined')
|
|
4867
5042
|
return;
|
|
5043
|
+
const newState = this._safeClone(state);
|
|
5044
|
+
// Try per-component time-travel via the component's STATE sink (reducer stream)
|
|
5045
|
+
const meta = this._components.get(componentId);
|
|
5046
|
+
if (meta) {
|
|
5047
|
+
const instance = meta._instanceRef?.deref();
|
|
5048
|
+
if (!instance) {
|
|
5049
|
+
console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
|
|
5050
|
+
}
|
|
5051
|
+
else {
|
|
5052
|
+
// sinks[stateSourceName] is the reducer stream — push a reducer that replaces state
|
|
5053
|
+
const stateSinkName = instance.stateSourceName || 'STATE';
|
|
5054
|
+
const stateSink = instance.sinks?.[stateSinkName];
|
|
5055
|
+
if (stateSink?.shamefullySendNext) {
|
|
5056
|
+
stateSink.shamefullySendNext(() => ({ ...newState }));
|
|
5057
|
+
this._post('TIME_TRAVEL_APPLIED', {
|
|
5058
|
+
componentId,
|
|
5059
|
+
componentName,
|
|
5060
|
+
state: newState,
|
|
5061
|
+
});
|
|
5062
|
+
return;
|
|
5063
|
+
}
|
|
5064
|
+
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'}`);
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
else {
|
|
5068
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
|
|
5069
|
+
}
|
|
5070
|
+
// Fall back to root STATE sink for root-level components
|
|
4868
5071
|
const app = window.__SYGNAL_DEVTOOLS_APP__;
|
|
4869
5072
|
if (app?.sinks?.STATE?.shamefullySendNext) {
|
|
4870
|
-
app.sinks.STATE.shamefullySendNext(() => ({ ...
|
|
5073
|
+
app.sinks.STATE.shamefullySendNext(() => ({ ...newState }));
|
|
4871
5074
|
this._post('TIME_TRAVEL_APPLIED', {
|
|
4872
|
-
|
|
4873
|
-
|
|
5075
|
+
componentId,
|
|
5076
|
+
componentName,
|
|
5077
|
+
state: newState,
|
|
4874
5078
|
});
|
|
4875
5079
|
}
|
|
5080
|
+
else {
|
|
5081
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
_takeSnapshot() {
|
|
5085
|
+
const entries = [];
|
|
5086
|
+
for (const [id, meta] of this._components) {
|
|
5087
|
+
if (meta.disposed)
|
|
5088
|
+
continue;
|
|
5089
|
+
const instance = meta._instanceRef?.deref();
|
|
5090
|
+
if (instance?.currentState != null) {
|
|
5091
|
+
entries.push({
|
|
5092
|
+
componentId: id,
|
|
5093
|
+
componentName: meta.name,
|
|
5094
|
+
state: this._safeClone(instance.currentState),
|
|
5095
|
+
});
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
this._post('SNAPSHOT_TAKEN', {
|
|
5099
|
+
entries,
|
|
5100
|
+
timestamp: Date.now(),
|
|
5101
|
+
});
|
|
5102
|
+
}
|
|
5103
|
+
_restoreSnapshot(snapshot) {
|
|
5104
|
+
if (!snapshot?.entries)
|
|
5105
|
+
return;
|
|
5106
|
+
for (const entry of snapshot.entries) {
|
|
5107
|
+
this._timeTravel(entry);
|
|
5108
|
+
}
|
|
4876
5109
|
}
|
|
4877
5110
|
_sendComponentState(componentId) {
|
|
4878
5111
|
const meta = this._components.get(componentId);
|
|
@@ -4883,11 +5116,38 @@ class SygnalDevTools {
|
|
|
4883
5116
|
componentId,
|
|
4884
5117
|
state: this._safeClone(instance.currentState),
|
|
4885
5118
|
context: this._safeClone(instance.currentContext),
|
|
5119
|
+
contextTrace: this._buildContextTrace(componentId, instance.currentContext),
|
|
4886
5120
|
props: this._safeClone(instance.currentProps),
|
|
4887
5121
|
});
|
|
4888
5122
|
}
|
|
4889
5123
|
}
|
|
4890
5124
|
}
|
|
5125
|
+
_buildContextTrace(componentId, context) {
|
|
5126
|
+
if (!context || typeof context !== 'object')
|
|
5127
|
+
return [];
|
|
5128
|
+
const trace = [];
|
|
5129
|
+
const fields = Object.keys(context);
|
|
5130
|
+
for (const field of fields) {
|
|
5131
|
+
// Walk up parent chain to find which component provides this field
|
|
5132
|
+
let currentId = componentId;
|
|
5133
|
+
let found = false;
|
|
5134
|
+
while (currentId != null) {
|
|
5135
|
+
const meta = this._components.get(currentId);
|
|
5136
|
+
if (!meta)
|
|
5137
|
+
break;
|
|
5138
|
+
if (meta.mviGraph?.contextProvides?.includes(field)) {
|
|
5139
|
+
trace.push({ field, providerId: meta.id, providerName: meta.name });
|
|
5140
|
+
found = true;
|
|
5141
|
+
break;
|
|
5142
|
+
}
|
|
5143
|
+
currentId = meta.parentId;
|
|
5144
|
+
}
|
|
5145
|
+
if (!found) {
|
|
5146
|
+
trace.push({ field, providerId: -1, providerName: 'unknown' });
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
return trace;
|
|
5150
|
+
}
|
|
4891
5151
|
_sendFullTree() {
|
|
4892
5152
|
const tree = [];
|
|
4893
5153
|
for (const [, meta] of this._components) {
|
|
@@ -4922,6 +5182,44 @@ class SygnalDevTools {
|
|
|
4922
5182
|
return '[unserializable]';
|
|
4923
5183
|
}
|
|
4924
5184
|
}
|
|
5185
|
+
_extractMviGraph(instance) {
|
|
5186
|
+
if (!instance.model)
|
|
5187
|
+
return null;
|
|
5188
|
+
try {
|
|
5189
|
+
const sources = instance.sourceNames ? [...instance.sourceNames] : [];
|
|
5190
|
+
const actions = [];
|
|
5191
|
+
const model = instance.model;
|
|
5192
|
+
for (const key of Object.keys(model)) {
|
|
5193
|
+
let actionName = key;
|
|
5194
|
+
let entry = model[key];
|
|
5195
|
+
// Handle shorthand 'ACTION | SINK'
|
|
5196
|
+
if (key.includes('|')) {
|
|
5197
|
+
const parts = key.split('|').map((s) => s.trim());
|
|
5198
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
5199
|
+
actionName = parts[0];
|
|
5200
|
+
actions.push({ name: actionName, sinks: [parts[1]] });
|
|
5201
|
+
continue;
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
// Function → targets STATE
|
|
5205
|
+
if (typeof entry === 'function') {
|
|
5206
|
+
actions.push({ name: actionName, sinks: [instance.stateSourceName || 'STATE'] });
|
|
5207
|
+
continue;
|
|
5208
|
+
}
|
|
5209
|
+
// Object → keys are sink names
|
|
5210
|
+
if (entry && typeof entry === 'object') {
|
|
5211
|
+
actions.push({ name: actionName, sinks: Object.keys(entry) });
|
|
5212
|
+
continue;
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
const contextProvides = instance.context && typeof instance.context === 'object'
|
|
5216
|
+
? Object.keys(instance.context) : [];
|
|
5217
|
+
return { sources, actions, contextProvides };
|
|
5218
|
+
}
|
|
5219
|
+
catch (e) {
|
|
5220
|
+
return null;
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
4925
5223
|
_serializeMeta(meta) {
|
|
4926
5224
|
const { _instanceRef, ...rest } = meta;
|
|
4927
5225
|
return rest;
|
|
@@ -5825,6 +6123,210 @@ function renderComponent(componentDef, options = {}) {
|
|
|
5825
6123
|
};
|
|
5826
6124
|
}
|
|
5827
6125
|
|
|
6126
|
+
/**
|
|
6127
|
+
* Reducer helpers for common state update patterns.
|
|
6128
|
+
*
|
|
6129
|
+
* These reduce boilerplate in model definitions by providing
|
|
6130
|
+
* shorthand factories for the most frequent reducer shapes.
|
|
6131
|
+
*/
|
|
6132
|
+
// ── set() ──────────────────────────────────────────────────────────
|
|
6133
|
+
/**
|
|
6134
|
+
* Create a reducer that merges a partial update into state.
|
|
6135
|
+
*
|
|
6136
|
+
* Static form — merge a fixed object:
|
|
6137
|
+
* set({ isEditing: true })
|
|
6138
|
+
*
|
|
6139
|
+
* Dynamic form — function receives (state, data, next, props) and
|
|
6140
|
+
* returns the partial update to merge:
|
|
6141
|
+
* set((state, title) => ({ title }))
|
|
6142
|
+
*/
|
|
6143
|
+
function set(partial) {
|
|
6144
|
+
if (typeof partial === 'function') {
|
|
6145
|
+
return (state, data, next, props) => ({
|
|
6146
|
+
...state,
|
|
6147
|
+
...partial(state, data, next, props),
|
|
6148
|
+
});
|
|
6149
|
+
}
|
|
6150
|
+
return (state) => ({ ...state, ...partial });
|
|
6151
|
+
}
|
|
6152
|
+
// ── toggle() ───────────────────────────────────────────────────────
|
|
6153
|
+
/**
|
|
6154
|
+
* Create a reducer that toggles a boolean field on state.
|
|
6155
|
+
*
|
|
6156
|
+
* toggle('showModal')
|
|
6157
|
+
* // equivalent to: (state) => ({ ...state, showModal: !state.showModal })
|
|
6158
|
+
*/
|
|
6159
|
+
function toggle(field) {
|
|
6160
|
+
return (state) => ({ ...state, [field]: !state[field] });
|
|
6161
|
+
}
|
|
6162
|
+
// ── emit() ─────────────────────────────────────────────────────────
|
|
6163
|
+
/**
|
|
6164
|
+
* Create a model entry that emits an EVENTS bus event.
|
|
6165
|
+
*
|
|
6166
|
+
* With static data:
|
|
6167
|
+
* emit('DELETE_LANE', { laneId: 42 })
|
|
6168
|
+
*
|
|
6169
|
+
* With dynamic data derived from state:
|
|
6170
|
+
* emit('DELETE_LANE', (state) => ({ laneId: state.id }))
|
|
6171
|
+
*
|
|
6172
|
+
* Fire-and-forget (no data):
|
|
6173
|
+
* emit('REFRESH')
|
|
6174
|
+
*/
|
|
6175
|
+
function emit(type, data) {
|
|
6176
|
+
return {
|
|
6177
|
+
EVENTS: typeof data === 'function'
|
|
6178
|
+
? (state, actionData, next, props) => ({ type, data: data(state, actionData, next, props) })
|
|
6179
|
+
: () => ({ type, data }),
|
|
6180
|
+
};
|
|
6181
|
+
}
|
|
6182
|
+
|
|
6183
|
+
// ── makeServiceWorkerDriver ──────────────────────────────────
|
|
6184
|
+
function trackWorker(worker, events) {
|
|
6185
|
+
const emit = (type, data) => events.dispatchEvent(new CustomEvent('data', { detail: { type, data } }));
|
|
6186
|
+
worker.addEventListener('statechange', () => {
|
|
6187
|
+
if (worker.state === 'installed')
|
|
6188
|
+
emit('installed', true);
|
|
6189
|
+
if (worker.state === 'activated')
|
|
6190
|
+
emit('activated', true);
|
|
6191
|
+
});
|
|
6192
|
+
if (worker.state === 'installed')
|
|
6193
|
+
emit('waiting', worker);
|
|
6194
|
+
if (worker.state === 'activated')
|
|
6195
|
+
emit('activated', true);
|
|
6196
|
+
}
|
|
6197
|
+
function makeServiceWorkerDriver(scriptUrl, options = {}) {
|
|
6198
|
+
return function serviceWorkerDriver(sink$) {
|
|
6199
|
+
const events = new EventTarget();
|
|
6200
|
+
if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
|
|
6201
|
+
navigator.serviceWorker
|
|
6202
|
+
.register(scriptUrl, { scope: options.scope })
|
|
6203
|
+
.then((reg) => {
|
|
6204
|
+
const emit = (type, data) => events.dispatchEvent(new CustomEvent('data', { detail: { type, data } }));
|
|
6205
|
+
if (reg.installing)
|
|
6206
|
+
trackWorker(reg.installing, events);
|
|
6207
|
+
if (reg.waiting)
|
|
6208
|
+
emit('waiting', reg.waiting);
|
|
6209
|
+
if (reg.active)
|
|
6210
|
+
emit('activated', true);
|
|
6211
|
+
reg.addEventListener('updatefound', () => {
|
|
6212
|
+
if (reg.installing)
|
|
6213
|
+
trackWorker(reg.installing, events);
|
|
6214
|
+
});
|
|
6215
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
6216
|
+
emit('controlling', true);
|
|
6217
|
+
});
|
|
6218
|
+
navigator.serviceWorker.addEventListener('message', (e) => {
|
|
6219
|
+
emit('message', e.data);
|
|
6220
|
+
});
|
|
6221
|
+
})
|
|
6222
|
+
.catch((err) => {
|
|
6223
|
+
events.dispatchEvent(new CustomEvent('data', { detail: { type: 'error', data: err } }));
|
|
6224
|
+
});
|
|
6225
|
+
sink$.addListener({
|
|
6226
|
+
next: (cmd) => {
|
|
6227
|
+
if (cmd.action === 'skipWaiting') {
|
|
6228
|
+
navigator.serviceWorker.ready.then((r) => {
|
|
6229
|
+
if (r.waiting)
|
|
6230
|
+
r.waiting.postMessage({ type: 'SKIP_WAITING' });
|
|
6231
|
+
});
|
|
6232
|
+
}
|
|
6233
|
+
else if (cmd.action === 'postMessage') {
|
|
6234
|
+
navigator.serviceWorker.ready.then((r) => {
|
|
6235
|
+
if (r.active)
|
|
6236
|
+
r.active.postMessage(cmd.data);
|
|
6237
|
+
});
|
|
6238
|
+
}
|
|
6239
|
+
else if (cmd.action === 'unregister') {
|
|
6240
|
+
navigator.serviceWorker.ready.then((r) => r.unregister());
|
|
6241
|
+
}
|
|
6242
|
+
},
|
|
6243
|
+
error: (err) => console.error('[SW driver] Error in sink stream:', err),
|
|
6244
|
+
});
|
|
6245
|
+
}
|
|
6246
|
+
return {
|
|
6247
|
+
select(type) {
|
|
6248
|
+
let cb;
|
|
6249
|
+
const in$ = xs__default.create({
|
|
6250
|
+
start: (listener) => {
|
|
6251
|
+
cb = ({ detail }) => {
|
|
6252
|
+
if (!type || detail.type === type)
|
|
6253
|
+
listener.next(detail.data);
|
|
6254
|
+
};
|
|
6255
|
+
events.addEventListener('data', cb);
|
|
6256
|
+
},
|
|
6257
|
+
stop: () => {
|
|
6258
|
+
if (cb)
|
|
6259
|
+
events.removeEventListener('data', cb);
|
|
6260
|
+
},
|
|
6261
|
+
});
|
|
6262
|
+
return adapt(in$);
|
|
6263
|
+
},
|
|
6264
|
+
};
|
|
6265
|
+
};
|
|
6266
|
+
}
|
|
6267
|
+
// ── onlineStatus$ ────────────────────────────────────────────
|
|
6268
|
+
function onlineStatus$() {
|
|
6269
|
+
if (typeof window === 'undefined') {
|
|
6270
|
+
return xs__default.of(true);
|
|
6271
|
+
}
|
|
6272
|
+
let cleanup;
|
|
6273
|
+
return xs__default.create({
|
|
6274
|
+
start(listener) {
|
|
6275
|
+
listener.next(navigator.onLine);
|
|
6276
|
+
const on = () => listener.next(true);
|
|
6277
|
+
const off = () => listener.next(false);
|
|
6278
|
+
window.addEventListener('online', on);
|
|
6279
|
+
window.addEventListener('offline', off);
|
|
6280
|
+
cleanup = () => {
|
|
6281
|
+
window.removeEventListener('online', on);
|
|
6282
|
+
window.removeEventListener('offline', off);
|
|
6283
|
+
};
|
|
6284
|
+
},
|
|
6285
|
+
stop() {
|
|
6286
|
+
cleanup?.();
|
|
6287
|
+
cleanup = undefined;
|
|
6288
|
+
},
|
|
6289
|
+
});
|
|
6290
|
+
}
|
|
6291
|
+
// ── createInstallPrompt ──────────────────────────────────────
|
|
6292
|
+
function createInstallPrompt() {
|
|
6293
|
+
let deferredPrompt = null;
|
|
6294
|
+
const events = new EventTarget();
|
|
6295
|
+
if (typeof window !== 'undefined') {
|
|
6296
|
+
window.addEventListener('beforeinstallprompt', (e) => {
|
|
6297
|
+
e.preventDefault();
|
|
6298
|
+
deferredPrompt = e;
|
|
6299
|
+
events.dispatchEvent(new CustomEvent('data', { detail: { type: 'beforeinstallprompt', data: true } }));
|
|
6300
|
+
});
|
|
6301
|
+
window.addEventListener('appinstalled', () => {
|
|
6302
|
+
deferredPrompt = null;
|
|
6303
|
+
events.dispatchEvent(new CustomEvent('data', { detail: { type: 'appinstalled', data: true } }));
|
|
6304
|
+
});
|
|
6305
|
+
}
|
|
6306
|
+
return {
|
|
6307
|
+
select(type) {
|
|
6308
|
+
let cb;
|
|
6309
|
+
const in$ = xs__default.create({
|
|
6310
|
+
start: (listener) => {
|
|
6311
|
+
cb = ({ detail }) => {
|
|
6312
|
+
if (detail.type === type)
|
|
6313
|
+
listener.next(detail.data);
|
|
6314
|
+
};
|
|
6315
|
+
events.addEventListener('data', cb);
|
|
6316
|
+
},
|
|
6317
|
+
stop: () => {
|
|
6318
|
+
if (cb)
|
|
6319
|
+
events.removeEventListener('data', cb);
|
|
6320
|
+
},
|
|
6321
|
+
});
|
|
6322
|
+
return adapt(in$);
|
|
6323
|
+
},
|
|
6324
|
+
prompt() {
|
|
6325
|
+
return deferredPrompt?.prompt();
|
|
6326
|
+
},
|
|
6327
|
+
};
|
|
6328
|
+
}
|
|
6329
|
+
|
|
5828
6330
|
/**
|
|
5829
6331
|
* Server-Side Rendering utilities for Sygnal components.
|
|
5830
6332
|
*
|
|
@@ -5995,6 +6497,24 @@ function processSSRTree(vnode, context, parentState) {
|
|
|
5995
6497
|
key: undefined,
|
|
5996
6498
|
};
|
|
5997
6499
|
}
|
|
6500
|
+
// ClientOnly: render fallback during SSR, skip children (they need a browser)
|
|
6501
|
+
if (sel === 'clientonly') {
|
|
6502
|
+
const props = vnode.data?.props || {};
|
|
6503
|
+
const fallback = props.fallback;
|
|
6504
|
+
if (fallback) {
|
|
6505
|
+
// fallback can be a VNode or a string
|
|
6506
|
+
return processSSRTree(fallback, context, parentState);
|
|
6507
|
+
}
|
|
6508
|
+
// No fallback — render an empty placeholder div
|
|
6509
|
+
return {
|
|
6510
|
+
sel: 'div',
|
|
6511
|
+
data: { attrs: { 'data-sygnal-clientonly': '' } },
|
|
6512
|
+
children: [],
|
|
6513
|
+
text: undefined,
|
|
6514
|
+
elm: undefined,
|
|
6515
|
+
key: undefined,
|
|
6516
|
+
};
|
|
6517
|
+
}
|
|
5998
6518
|
// Slot: unwrap to children
|
|
5999
6519
|
if (sel === 'slot') {
|
|
6000
6520
|
const children = vnode.children || [];
|
|
@@ -6474,4 +6994,4 @@ function buildAttributes(data, selectorId, selectorClasses) {
|
|
|
6474
6994
|
return result;
|
|
6475
6995
|
}
|
|
6476
6996
|
|
|
6477
|
-
export { ABORT, Collection, MainDOMSource, MockedDOMSource, Portal, Slot, Suspense, Switchable, Transition, classes, collection, component, createCommand, createElement, createRef, createRef$, driverFromAsync, enableHMR, exactState, getDevTools, lazy, makeDOMDriver, makeDragDriver, mockDOMSource, Portal as portal, processDrag, processForm, renderComponent, renderToString, run, switchable, thunk, xs };
|
|
6997
|
+
export { ABORT, Collection, MainDOMSource, MockedDOMSource, Portal, Slot, Suspense, Switchable, Transition, classes, collection, component, createCommand, createElement, createInstallPrompt, createRef, createRef$, driverFromAsync, emit, enableHMR, exactState, getDevTools, lazy, makeDOMDriver, makeDragDriver, makeServiceWorkerDriver, mockDOMSource, onlineStatus$, Portal as portal, processDrag, processForm, renderComponent, renderToString, run, set, switchable, thunk, toggle, xs };
|