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.cjs.js
CHANGED
|
@@ -2163,11 +2163,17 @@ function createCommand() {
|
|
|
2163
2163
|
start(l) { listener.next = (val) => l.next(val); },
|
|
2164
2164
|
stop() { listener.next = () => { }; },
|
|
2165
2165
|
});
|
|
2166
|
-
|
|
2167
|
-
send: (type, data) =>
|
|
2166
|
+
const cmd = {
|
|
2167
|
+
send: (type, data) => {
|
|
2168
|
+
listener.next({ type, data });
|
|
2169
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
2170
|
+
window.__SYGNAL_DEVTOOLS__.onCommandSent(type, data, cmd._targetComponentId, cmd._targetComponentName);
|
|
2171
|
+
}
|
|
2172
|
+
},
|
|
2168
2173
|
_stream,
|
|
2169
2174
|
__sygnalCommand: true,
|
|
2170
2175
|
};
|
|
2176
|
+
return cmd;
|
|
2171
2177
|
}
|
|
2172
2178
|
function makeCommandSource(cmd) {
|
|
2173
2179
|
return {
|
|
@@ -2270,7 +2276,7 @@ function component(opts) {
|
|
|
2270
2276
|
return returnFunction;
|
|
2271
2277
|
}
|
|
2272
2278
|
class Component {
|
|
2273
|
-
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 }) {
|
|
2279
|
+
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 }) {
|
|
2274
2280
|
if (!sources || !isObj(sources))
|
|
2275
2281
|
throw new Error(`[${name}] Missing or invalid sources`);
|
|
2276
2282
|
this._componentNumber = COMPONENT_COUNT++;
|
|
@@ -2292,6 +2298,7 @@ class Component {
|
|
|
2292
2298
|
this.requestSourceName = requestSourceName;
|
|
2293
2299
|
this.sourceNames = Object.keys(sources);
|
|
2294
2300
|
this.onError = onError;
|
|
2301
|
+
this.isolatedState = isolatedState;
|
|
2295
2302
|
this._debug = debug;
|
|
2296
2303
|
// Warn if calculated fields shadow base state keys
|
|
2297
2304
|
if (this.calculated && this.initialState
|
|
@@ -2420,6 +2427,9 @@ class Component {
|
|
|
2420
2427
|
this.sources.props$ = props$.map((val) => {
|
|
2421
2428
|
const { sygnalFactory, sygnalOptions, ...sanitizedProps } = val;
|
|
2422
2429
|
this.currentProps = sanitizedProps;
|
|
2430
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
2431
|
+
window.__SYGNAL_DEVTOOLS__.onPropsChanged(this._componentNumber, this.name, sanitizedProps);
|
|
2432
|
+
}
|
|
2423
2433
|
return val;
|
|
2424
2434
|
});
|
|
2425
2435
|
}
|
|
@@ -2491,6 +2501,9 @@ class Component {
|
|
|
2491
2501
|
}
|
|
2492
2502
|
}
|
|
2493
2503
|
dispose() {
|
|
2504
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
2505
|
+
window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name);
|
|
2506
|
+
}
|
|
2494
2507
|
// Fire the DISPOSE built-in action so model handlers can run cleanup logic
|
|
2495
2508
|
const hasDispose = this.model && (this.model[DISPOSE_ACTION] || Object.keys(this.model).some(k => k.includes('|') && k.split('|')[0].trim() === DISPOSE_ACTION));
|
|
2496
2509
|
if (hasDispose && this.action$ && typeof this.action$.shamefullySendNext === 'function') {
|
|
@@ -2691,7 +2704,7 @@ class Component {
|
|
|
2691
2704
|
const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE;
|
|
2692
2705
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
|
|
2693
2706
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
|
|
2694
|
-
if (this.isSubComponent && this.initialState) {
|
|
2707
|
+
if (this.isSubComponent && this.initialState && !this.isolatedState) {
|
|
2695
2708
|
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
|
|
2696
2709
|
}
|
|
2697
2710
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined');
|
|
@@ -2872,6 +2885,7 @@ class Component {
|
|
|
2872
2885
|
.map((vdom) => processLazy(vdom, this))
|
|
2873
2886
|
.map(processPortals)
|
|
2874
2887
|
.map(processTransitions)
|
|
2888
|
+
.map(processClientOnly)
|
|
2875
2889
|
.compose(this.instantiateSubComponents.bind(this))
|
|
2876
2890
|
.filter((val) => val !== undefined)
|
|
2877
2891
|
.compose(this.renderVdom.bind(this));
|
|
@@ -2887,6 +2901,12 @@ class Component {
|
|
|
2887
2901
|
else {
|
|
2888
2902
|
acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []));
|
|
2889
2903
|
}
|
|
2904
|
+
// Stamp EVENTS sink emissions with emitter component info for devtools
|
|
2905
|
+
if (name === 'EVENTS' && acc[name]) {
|
|
2906
|
+
const _componentNumber = this._componentNumber;
|
|
2907
|
+
const _name = this.name;
|
|
2908
|
+
acc[name] = acc[name].map((ev) => ({ ...ev, __emitterId: _componentNumber, __emitterName: _name }));
|
|
2909
|
+
}
|
|
2890
2910
|
return acc;
|
|
2891
2911
|
}, {});
|
|
2892
2912
|
this.sinks[this.DOMSourceName] = this.vdom$;
|
|
@@ -3409,6 +3429,13 @@ class Component {
|
|
|
3409
3429
|
if (!isObj(sink$)) {
|
|
3410
3430
|
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`);
|
|
3411
3431
|
}
|
|
3432
|
+
// Notify devtools of collection mount
|
|
3433
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3434
|
+
const itemName = typeof collectionOf === 'function'
|
|
3435
|
+
? (collectionOf.componentName || collectionOf.label || collectionOf.name || 'anonymous')
|
|
3436
|
+
: String(collectionOf);
|
|
3437
|
+
window.__SYGNAL_DEVTOOLS__.onCollectionMounted(this._componentNumber, this.name, itemName, typeof stateField === 'string' ? stateField : null);
|
|
3438
|
+
}
|
|
3412
3439
|
return sink$;
|
|
3413
3440
|
}
|
|
3414
3441
|
instantiateSwitchable(el, props$, children$) {
|
|
@@ -3547,6 +3574,7 @@ class Component {
|
|
|
3547
3574
|
for (const key of Object.keys(props)) {
|
|
3548
3575
|
const val = props[key];
|
|
3549
3576
|
if (val && val.__sygnalCommand) {
|
|
3577
|
+
val._targetComponentName = componentName;
|
|
3550
3578
|
sources.commands$ = makeCommandSource(val);
|
|
3551
3579
|
break;
|
|
3552
3580
|
}
|
|
@@ -3589,10 +3617,15 @@ class Component {
|
|
|
3589
3617
|
const wasReady = this._childReadyState[id];
|
|
3590
3618
|
this._childReadyState[id] = !!ready;
|
|
3591
3619
|
// When READY state changes, trigger a re-render
|
|
3592
|
-
if (wasReady !== !!ready
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3620
|
+
if (wasReady !== !!ready) {
|
|
3621
|
+
if (this._readyChangedListener) {
|
|
3622
|
+
setTimeout(() => {
|
|
3623
|
+
this._readyChangedListener?.next(null);
|
|
3624
|
+
}, 0);
|
|
3625
|
+
}
|
|
3626
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3627
|
+
window.__SYGNAL_DEVTOOLS__.onReadyChanged(this._componentNumber, this.name, id, !!ready);
|
|
3628
|
+
}
|
|
3596
3629
|
}
|
|
3597
3630
|
},
|
|
3598
3631
|
error: () => { },
|
|
@@ -3849,6 +3882,31 @@ function processTransitions(vnode) {
|
|
|
3849
3882
|
}
|
|
3850
3883
|
return vnode;
|
|
3851
3884
|
}
|
|
3885
|
+
function processClientOnly(vnode) {
|
|
3886
|
+
if (!vnode || !vnode.sel)
|
|
3887
|
+
return vnode;
|
|
3888
|
+
if (vnode.sel === 'clientonly') {
|
|
3889
|
+
// On the client, unwrap to children (render them normally)
|
|
3890
|
+
const children = vnode.children || [];
|
|
3891
|
+
if (children.length === 0)
|
|
3892
|
+
return { sel: 'div', data: {}, children: [] };
|
|
3893
|
+
if (children.length === 1)
|
|
3894
|
+
return processClientOnly(children[0]);
|
|
3895
|
+
// Multiple children: wrap in a div
|
|
3896
|
+
return {
|
|
3897
|
+
sel: 'div',
|
|
3898
|
+
data: {},
|
|
3899
|
+
children: children.map(processClientOnly),
|
|
3900
|
+
text: undefined,
|
|
3901
|
+
elm: undefined,
|
|
3902
|
+
key: undefined,
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
if (vnode.children && vnode.children.length > 0) {
|
|
3906
|
+
vnode.children = vnode.children.map(processClientOnly);
|
|
3907
|
+
}
|
|
3908
|
+
return vnode;
|
|
3909
|
+
}
|
|
3852
3910
|
function applyTransitionHooks(vnode, name, duration) {
|
|
3853
3911
|
const existingInsert = vnode.data?.hook?.insert;
|
|
3854
3912
|
const existingRemove = vnode.data?.hook?.remove;
|
|
@@ -4408,6 +4466,40 @@ function processDrag({ draggable, dropZone } = {}, options = {}) {
|
|
|
4408
4466
|
return { dragStart$, dragEnd$, dragOver$, drop$ };
|
|
4409
4467
|
}
|
|
4410
4468
|
|
|
4469
|
+
/**
|
|
4470
|
+
* Adds chainable convenience methods to a DND event stream,
|
|
4471
|
+
* mirroring the DOM driver's `enrichEventStream` pattern.
|
|
4472
|
+
*
|
|
4473
|
+
* DND.dragstart('task').data('taskId')
|
|
4474
|
+
* DND.dragstart('task').data('taskId', Number)
|
|
4475
|
+
* DND.drop('lane').data('laneId')
|
|
4476
|
+
* DND.dragstart('task').element()
|
|
4477
|
+
*/
|
|
4478
|
+
function enrichDragStream(stream$) {
|
|
4479
|
+
// .data(name, fn?) — extract dataset[name] from dragstart payload,
|
|
4480
|
+
// or dropZone.dataset[name] from drop payload
|
|
4481
|
+
stream$.data = function data(name, fn) {
|
|
4482
|
+
const mapped = stream$.map((e) => {
|
|
4483
|
+
// dragstart payload: { element, dataset }
|
|
4484
|
+
// drop payload: { dropZone, insertBefore }
|
|
4485
|
+
const val = e?.dataset?.[name]
|
|
4486
|
+
?? e?.dropZone?.dataset?.[name]
|
|
4487
|
+
?? e?.element?.dataset?.[name];
|
|
4488
|
+
return fn ? fn(val) : val;
|
|
4489
|
+
});
|
|
4490
|
+
return enrichDragStream(mapped);
|
|
4491
|
+
};
|
|
4492
|
+
// .element(fn?) — extract the primary element from the payload
|
|
4493
|
+
stream$.element = function element(fn) {
|
|
4494
|
+
const mapped = stream$.map((e) => {
|
|
4495
|
+
const el = e?.element ?? e?.dropZone ?? null;
|
|
4496
|
+
return fn ? fn(el) : el;
|
|
4497
|
+
});
|
|
4498
|
+
return enrichDragStream(mapped);
|
|
4499
|
+
};
|
|
4500
|
+
return stream$;
|
|
4501
|
+
}
|
|
4502
|
+
// ─── Driver Factory ──────────────────────────────────────────────────────────
|
|
4411
4503
|
function makeDragDriver() {
|
|
4412
4504
|
return function dragDriver(sink$) {
|
|
4413
4505
|
const categories = new Map();
|
|
@@ -4499,7 +4591,7 @@ function makeDragDriver() {
|
|
|
4499
4591
|
events(eventType) {
|
|
4500
4592
|
const busEventName = `${category}:${eventType}`;
|
|
4501
4593
|
let handler;
|
|
4502
|
-
|
|
4594
|
+
const stream$ = xs$1.create({
|
|
4503
4595
|
start(listener) {
|
|
4504
4596
|
handler = ({ detail }) => listener.next(detail);
|
|
4505
4597
|
bus.addEventListener(busEventName, handler);
|
|
@@ -4509,6 +4601,7 @@ function makeDragDriver() {
|
|
|
4509
4601
|
bus.removeEventListener(busEventName, handler);
|
|
4510
4602
|
},
|
|
4511
4603
|
});
|
|
4604
|
+
return enrichDragStream(stream$);
|
|
4512
4605
|
},
|
|
4513
4606
|
};
|
|
4514
4607
|
},
|
|
@@ -4688,7 +4781,12 @@ function setupReusable(drivers) {
|
|
|
4688
4781
|
function eventBusDriver(out$) {
|
|
4689
4782
|
const events = new EventTarget();
|
|
4690
4783
|
out$.subscribe({
|
|
4691
|
-
next: (event) =>
|
|
4784
|
+
next: (event) => {
|
|
4785
|
+
events.dispatchEvent(new CustomEvent('data', { detail: event }));
|
|
4786
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
4787
|
+
window.__SYGNAL_DEVTOOLS__.onBusEvent(event);
|
|
4788
|
+
}
|
|
4789
|
+
},
|
|
4692
4790
|
error: (err) => console.error('[EVENTS driver] Error in sink stream:', err),
|
|
4693
4791
|
});
|
|
4694
4792
|
return {
|
|
@@ -4768,6 +4866,12 @@ class SygnalDevTools {
|
|
|
4768
4866
|
case 'TIME_TRAVEL':
|
|
4769
4867
|
this._timeTravel(msg.payload);
|
|
4770
4868
|
break;
|
|
4869
|
+
case 'SNAPSHOT':
|
|
4870
|
+
this._takeSnapshot();
|
|
4871
|
+
break;
|
|
4872
|
+
case 'RESTORE_SNAPSHOT':
|
|
4873
|
+
this._restoreSnapshot(msg.payload);
|
|
4874
|
+
break;
|
|
4771
4875
|
case 'GET_STATE':
|
|
4772
4876
|
this._sendComponentState(msg.payload.componentId);
|
|
4773
4877
|
break;
|
|
@@ -4787,6 +4891,7 @@ class SygnalDevTools {
|
|
|
4787
4891
|
children: [],
|
|
4788
4892
|
debug: instance._debug,
|
|
4789
4893
|
createdAt: Date.now(),
|
|
4894
|
+
mviGraph: this._extractMviGraph(instance),
|
|
4790
4895
|
_instanceRef: new WeakRef(instance),
|
|
4791
4896
|
};
|
|
4792
4897
|
this._components.set(componentNumber, meta);
|
|
@@ -4811,7 +4916,6 @@ class SygnalDevTools {
|
|
|
4811
4916
|
componentId: componentNumber,
|
|
4812
4917
|
componentName: name,
|
|
4813
4918
|
state: entry.state,
|
|
4814
|
-
historyIndex: this._stateHistory.length - 1,
|
|
4815
4919
|
});
|
|
4816
4920
|
}
|
|
4817
4921
|
onActionDispatched(componentNumber, name, actionType, data) {
|
|
@@ -4848,6 +4952,76 @@ class SygnalDevTools {
|
|
|
4848
4952
|
componentId: componentNumber,
|
|
4849
4953
|
componentName: name,
|
|
4850
4954
|
context: this._safeClone(context),
|
|
4955
|
+
contextTrace: this._buildContextTrace(componentNumber, context),
|
|
4956
|
+
});
|
|
4957
|
+
}
|
|
4958
|
+
onPropsChanged(componentNumber, name, props) {
|
|
4959
|
+
if (!this.connected)
|
|
4960
|
+
return;
|
|
4961
|
+
this._post('PROPS_CHANGED', {
|
|
4962
|
+
componentId: componentNumber,
|
|
4963
|
+
componentName: name,
|
|
4964
|
+
props: this._safeClone(props),
|
|
4965
|
+
});
|
|
4966
|
+
}
|
|
4967
|
+
onBusEvent(event) {
|
|
4968
|
+
if (!this.connected)
|
|
4969
|
+
return;
|
|
4970
|
+
this._post('BUS_EVENT', {
|
|
4971
|
+
type: event.type,
|
|
4972
|
+
data: this._safeClone(event.data),
|
|
4973
|
+
componentId: event.__emitterId ?? null,
|
|
4974
|
+
componentName: event.__emitterName ?? null,
|
|
4975
|
+
timestamp: Date.now(),
|
|
4976
|
+
});
|
|
4977
|
+
}
|
|
4978
|
+
onCommandSent(type, data, targetComponentId, targetComponentName) {
|
|
4979
|
+
if (!this.connected)
|
|
4980
|
+
return;
|
|
4981
|
+
this._post('COMMAND_SENT', {
|
|
4982
|
+
type,
|
|
4983
|
+
data: this._safeClone(data),
|
|
4984
|
+
targetComponentName: targetComponentName ?? null,
|
|
4985
|
+
timestamp: Date.now(),
|
|
4986
|
+
});
|
|
4987
|
+
}
|
|
4988
|
+
onReadyChanged(parentId, parentName, childKey, ready) {
|
|
4989
|
+
if (!this.connected)
|
|
4990
|
+
return;
|
|
4991
|
+
this._post('READY_CHANGED', {
|
|
4992
|
+
parentId,
|
|
4993
|
+
parentName,
|
|
4994
|
+
childKey,
|
|
4995
|
+
ready,
|
|
4996
|
+
timestamp: Date.now(),
|
|
4997
|
+
});
|
|
4998
|
+
}
|
|
4999
|
+
onCollectionMounted(parentId, parentName, itemComponentName, stateField) {
|
|
5000
|
+
const meta = this._components.get(parentId);
|
|
5001
|
+
if (meta) {
|
|
5002
|
+
meta.collection = { itemComponent: itemComponentName, stateField };
|
|
5003
|
+
}
|
|
5004
|
+
if (!this.connected)
|
|
5005
|
+
return;
|
|
5006
|
+
this._post('COLLECTION_MOUNTED', {
|
|
5007
|
+
parentId,
|
|
5008
|
+
parentName,
|
|
5009
|
+
itemComponent: itemComponentName,
|
|
5010
|
+
stateField,
|
|
5011
|
+
});
|
|
5012
|
+
}
|
|
5013
|
+
onComponentDisposed(componentNumber, name) {
|
|
5014
|
+
const meta = this._components.get(componentNumber);
|
|
5015
|
+
if (meta) {
|
|
5016
|
+
meta.disposed = true;
|
|
5017
|
+
meta.disposedAt = Date.now();
|
|
5018
|
+
}
|
|
5019
|
+
if (!this.connected)
|
|
5020
|
+
return;
|
|
5021
|
+
this._post('COMPONENT_DISPOSED', {
|
|
5022
|
+
componentId: componentNumber,
|
|
5023
|
+
componentName: name,
|
|
5024
|
+
timestamp: Date.now(),
|
|
4851
5025
|
});
|
|
4852
5026
|
}
|
|
4853
5027
|
onDebugLog(componentNumber, message) {
|
|
@@ -4876,20 +5050,79 @@ class SygnalDevTools {
|
|
|
4876
5050
|
}
|
|
4877
5051
|
}
|
|
4878
5052
|
}
|
|
4879
|
-
_timeTravel({
|
|
4880
|
-
|
|
4881
|
-
|
|
5053
|
+
_timeTravel({ componentId, componentName, state }) {
|
|
5054
|
+
if (componentId == null || !state) {
|
|
5055
|
+
console.warn('[Sygnal DevTools] _timeTravel: missing componentId or state', { componentId, hasState: !!state });
|
|
4882
5056
|
return;
|
|
5057
|
+
}
|
|
4883
5058
|
if (typeof window === 'undefined')
|
|
4884
5059
|
return;
|
|
5060
|
+
const newState = this._safeClone(state);
|
|
5061
|
+
// Try per-component time-travel via the component's STATE sink (reducer stream)
|
|
5062
|
+
const meta = this._components.get(componentId);
|
|
5063
|
+
if (meta) {
|
|
5064
|
+
const instance = meta._instanceRef?.deref();
|
|
5065
|
+
if (!instance) {
|
|
5066
|
+
console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
|
|
5067
|
+
}
|
|
5068
|
+
else {
|
|
5069
|
+
// sinks[stateSourceName] is the reducer stream — push a reducer that replaces state
|
|
5070
|
+
const stateSinkName = instance.stateSourceName || 'STATE';
|
|
5071
|
+
const stateSink = instance.sinks?.[stateSinkName];
|
|
5072
|
+
if (stateSink?.shamefullySendNext) {
|
|
5073
|
+
stateSink.shamefullySendNext(() => ({ ...newState }));
|
|
5074
|
+
this._post('TIME_TRAVEL_APPLIED', {
|
|
5075
|
+
componentId,
|
|
5076
|
+
componentName,
|
|
5077
|
+
state: newState,
|
|
5078
|
+
});
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
5081
|
+
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'}`);
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
else {
|
|
5085
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
|
|
5086
|
+
}
|
|
5087
|
+
// Fall back to root STATE sink for root-level components
|
|
4885
5088
|
const app = window.__SYGNAL_DEVTOOLS_APP__;
|
|
4886
5089
|
if (app?.sinks?.STATE?.shamefullySendNext) {
|
|
4887
|
-
app.sinks.STATE.shamefullySendNext(() => ({ ...
|
|
5090
|
+
app.sinks.STATE.shamefullySendNext(() => ({ ...newState }));
|
|
4888
5091
|
this._post('TIME_TRAVEL_APPLIED', {
|
|
4889
|
-
|
|
4890
|
-
|
|
5092
|
+
componentId,
|
|
5093
|
+
componentName,
|
|
5094
|
+
state: newState,
|
|
4891
5095
|
});
|
|
4892
5096
|
}
|
|
5097
|
+
else {
|
|
5098
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
_takeSnapshot() {
|
|
5102
|
+
const entries = [];
|
|
5103
|
+
for (const [id, meta] of this._components) {
|
|
5104
|
+
if (meta.disposed)
|
|
5105
|
+
continue;
|
|
5106
|
+
const instance = meta._instanceRef?.deref();
|
|
5107
|
+
if (instance?.currentState != null) {
|
|
5108
|
+
entries.push({
|
|
5109
|
+
componentId: id,
|
|
5110
|
+
componentName: meta.name,
|
|
5111
|
+
state: this._safeClone(instance.currentState),
|
|
5112
|
+
});
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
this._post('SNAPSHOT_TAKEN', {
|
|
5116
|
+
entries,
|
|
5117
|
+
timestamp: Date.now(),
|
|
5118
|
+
});
|
|
5119
|
+
}
|
|
5120
|
+
_restoreSnapshot(snapshot) {
|
|
5121
|
+
if (!snapshot?.entries)
|
|
5122
|
+
return;
|
|
5123
|
+
for (const entry of snapshot.entries) {
|
|
5124
|
+
this._timeTravel(entry);
|
|
5125
|
+
}
|
|
4893
5126
|
}
|
|
4894
5127
|
_sendComponentState(componentId) {
|
|
4895
5128
|
const meta = this._components.get(componentId);
|
|
@@ -4900,11 +5133,38 @@ class SygnalDevTools {
|
|
|
4900
5133
|
componentId,
|
|
4901
5134
|
state: this._safeClone(instance.currentState),
|
|
4902
5135
|
context: this._safeClone(instance.currentContext),
|
|
5136
|
+
contextTrace: this._buildContextTrace(componentId, instance.currentContext),
|
|
4903
5137
|
props: this._safeClone(instance.currentProps),
|
|
4904
5138
|
});
|
|
4905
5139
|
}
|
|
4906
5140
|
}
|
|
4907
5141
|
}
|
|
5142
|
+
_buildContextTrace(componentId, context) {
|
|
5143
|
+
if (!context || typeof context !== 'object')
|
|
5144
|
+
return [];
|
|
5145
|
+
const trace = [];
|
|
5146
|
+
const fields = Object.keys(context);
|
|
5147
|
+
for (const field of fields) {
|
|
5148
|
+
// Walk up parent chain to find which component provides this field
|
|
5149
|
+
let currentId = componentId;
|
|
5150
|
+
let found = false;
|
|
5151
|
+
while (currentId != null) {
|
|
5152
|
+
const meta = this._components.get(currentId);
|
|
5153
|
+
if (!meta)
|
|
5154
|
+
break;
|
|
5155
|
+
if (meta.mviGraph?.contextProvides?.includes(field)) {
|
|
5156
|
+
trace.push({ field, providerId: meta.id, providerName: meta.name });
|
|
5157
|
+
found = true;
|
|
5158
|
+
break;
|
|
5159
|
+
}
|
|
5160
|
+
currentId = meta.parentId;
|
|
5161
|
+
}
|
|
5162
|
+
if (!found) {
|
|
5163
|
+
trace.push({ field, providerId: -1, providerName: 'unknown' });
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
return trace;
|
|
5167
|
+
}
|
|
4908
5168
|
_sendFullTree() {
|
|
4909
5169
|
const tree = [];
|
|
4910
5170
|
for (const [, meta] of this._components) {
|
|
@@ -4939,6 +5199,44 @@ class SygnalDevTools {
|
|
|
4939
5199
|
return '[unserializable]';
|
|
4940
5200
|
}
|
|
4941
5201
|
}
|
|
5202
|
+
_extractMviGraph(instance) {
|
|
5203
|
+
if (!instance.model)
|
|
5204
|
+
return null;
|
|
5205
|
+
try {
|
|
5206
|
+
const sources = instance.sourceNames ? [...instance.sourceNames] : [];
|
|
5207
|
+
const actions = [];
|
|
5208
|
+
const model = instance.model;
|
|
5209
|
+
for (const key of Object.keys(model)) {
|
|
5210
|
+
let actionName = key;
|
|
5211
|
+
let entry = model[key];
|
|
5212
|
+
// Handle shorthand 'ACTION | SINK'
|
|
5213
|
+
if (key.includes('|')) {
|
|
5214
|
+
const parts = key.split('|').map((s) => s.trim());
|
|
5215
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
5216
|
+
actionName = parts[0];
|
|
5217
|
+
actions.push({ name: actionName, sinks: [parts[1]] });
|
|
5218
|
+
continue;
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
// Function → targets STATE
|
|
5222
|
+
if (typeof entry === 'function') {
|
|
5223
|
+
actions.push({ name: actionName, sinks: [instance.stateSourceName || 'STATE'] });
|
|
5224
|
+
continue;
|
|
5225
|
+
}
|
|
5226
|
+
// Object → keys are sink names
|
|
5227
|
+
if (entry && typeof entry === 'object') {
|
|
5228
|
+
actions.push({ name: actionName, sinks: Object.keys(entry) });
|
|
5229
|
+
continue;
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
const contextProvides = instance.context && typeof instance.context === 'object'
|
|
5233
|
+
? Object.keys(instance.context) : [];
|
|
5234
|
+
return { sources, actions, contextProvides };
|
|
5235
|
+
}
|
|
5236
|
+
catch (e) {
|
|
5237
|
+
return null;
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
4942
5240
|
_serializeMeta(meta) {
|
|
4943
5241
|
const { _instanceRef, ...rest } = meta;
|
|
4944
5242
|
return rest;
|
|
@@ -5842,6 +6140,210 @@ function renderComponent(componentDef, options = {}) {
|
|
|
5842
6140
|
};
|
|
5843
6141
|
}
|
|
5844
6142
|
|
|
6143
|
+
/**
|
|
6144
|
+
* Reducer helpers for common state update patterns.
|
|
6145
|
+
*
|
|
6146
|
+
* These reduce boilerplate in model definitions by providing
|
|
6147
|
+
* shorthand factories for the most frequent reducer shapes.
|
|
6148
|
+
*/
|
|
6149
|
+
// ── set() ──────────────────────────────────────────────────────────
|
|
6150
|
+
/**
|
|
6151
|
+
* Create a reducer that merges a partial update into state.
|
|
6152
|
+
*
|
|
6153
|
+
* Static form — merge a fixed object:
|
|
6154
|
+
* set({ isEditing: true })
|
|
6155
|
+
*
|
|
6156
|
+
* Dynamic form — function receives (state, data, next, props) and
|
|
6157
|
+
* returns the partial update to merge:
|
|
6158
|
+
* set((state, title) => ({ title }))
|
|
6159
|
+
*/
|
|
6160
|
+
function set(partial) {
|
|
6161
|
+
if (typeof partial === 'function') {
|
|
6162
|
+
return (state, data, next, props) => ({
|
|
6163
|
+
...state,
|
|
6164
|
+
...partial(state, data, next, props),
|
|
6165
|
+
});
|
|
6166
|
+
}
|
|
6167
|
+
return (state) => ({ ...state, ...partial });
|
|
6168
|
+
}
|
|
6169
|
+
// ── toggle() ───────────────────────────────────────────────────────
|
|
6170
|
+
/**
|
|
6171
|
+
* Create a reducer that toggles a boolean field on state.
|
|
6172
|
+
*
|
|
6173
|
+
* toggle('showModal')
|
|
6174
|
+
* // equivalent to: (state) => ({ ...state, showModal: !state.showModal })
|
|
6175
|
+
*/
|
|
6176
|
+
function toggle(field) {
|
|
6177
|
+
return (state) => ({ ...state, [field]: !state[field] });
|
|
6178
|
+
}
|
|
6179
|
+
// ── emit() ─────────────────────────────────────────────────────────
|
|
6180
|
+
/**
|
|
6181
|
+
* Create a model entry that emits an EVENTS bus event.
|
|
6182
|
+
*
|
|
6183
|
+
* With static data:
|
|
6184
|
+
* emit('DELETE_LANE', { laneId: 42 })
|
|
6185
|
+
*
|
|
6186
|
+
* With dynamic data derived from state:
|
|
6187
|
+
* emit('DELETE_LANE', (state) => ({ laneId: state.id }))
|
|
6188
|
+
*
|
|
6189
|
+
* Fire-and-forget (no data):
|
|
6190
|
+
* emit('REFRESH')
|
|
6191
|
+
*/
|
|
6192
|
+
function emit(type, data) {
|
|
6193
|
+
return {
|
|
6194
|
+
EVENTS: typeof data === 'function'
|
|
6195
|
+
? (state, actionData, next, props) => ({ type, data: data(state, actionData, next, props) })
|
|
6196
|
+
: () => ({ type, data }),
|
|
6197
|
+
};
|
|
6198
|
+
}
|
|
6199
|
+
|
|
6200
|
+
// ── makeServiceWorkerDriver ──────────────────────────────────
|
|
6201
|
+
function trackWorker(worker, events) {
|
|
6202
|
+
const emit = (type, data) => events.dispatchEvent(new CustomEvent('data', { detail: { type, data } }));
|
|
6203
|
+
worker.addEventListener('statechange', () => {
|
|
6204
|
+
if (worker.state === 'installed')
|
|
6205
|
+
emit('installed', true);
|
|
6206
|
+
if (worker.state === 'activated')
|
|
6207
|
+
emit('activated', true);
|
|
6208
|
+
});
|
|
6209
|
+
if (worker.state === 'installed')
|
|
6210
|
+
emit('waiting', worker);
|
|
6211
|
+
if (worker.state === 'activated')
|
|
6212
|
+
emit('activated', true);
|
|
6213
|
+
}
|
|
6214
|
+
function makeServiceWorkerDriver(scriptUrl, options = {}) {
|
|
6215
|
+
return function serviceWorkerDriver(sink$) {
|
|
6216
|
+
const events = new EventTarget();
|
|
6217
|
+
if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
|
|
6218
|
+
navigator.serviceWorker
|
|
6219
|
+
.register(scriptUrl, { scope: options.scope })
|
|
6220
|
+
.then((reg) => {
|
|
6221
|
+
const emit = (type, data) => events.dispatchEvent(new CustomEvent('data', { detail: { type, data } }));
|
|
6222
|
+
if (reg.installing)
|
|
6223
|
+
trackWorker(reg.installing, events);
|
|
6224
|
+
if (reg.waiting)
|
|
6225
|
+
emit('waiting', reg.waiting);
|
|
6226
|
+
if (reg.active)
|
|
6227
|
+
emit('activated', true);
|
|
6228
|
+
reg.addEventListener('updatefound', () => {
|
|
6229
|
+
if (reg.installing)
|
|
6230
|
+
trackWorker(reg.installing, events);
|
|
6231
|
+
});
|
|
6232
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
6233
|
+
emit('controlling', true);
|
|
6234
|
+
});
|
|
6235
|
+
navigator.serviceWorker.addEventListener('message', (e) => {
|
|
6236
|
+
emit('message', e.data);
|
|
6237
|
+
});
|
|
6238
|
+
})
|
|
6239
|
+
.catch((err) => {
|
|
6240
|
+
events.dispatchEvent(new CustomEvent('data', { detail: { type: 'error', data: err } }));
|
|
6241
|
+
});
|
|
6242
|
+
sink$.addListener({
|
|
6243
|
+
next: (cmd) => {
|
|
6244
|
+
if (cmd.action === 'skipWaiting') {
|
|
6245
|
+
navigator.serviceWorker.ready.then((r) => {
|
|
6246
|
+
if (r.waiting)
|
|
6247
|
+
r.waiting.postMessage({ type: 'SKIP_WAITING' });
|
|
6248
|
+
});
|
|
6249
|
+
}
|
|
6250
|
+
else if (cmd.action === 'postMessage') {
|
|
6251
|
+
navigator.serviceWorker.ready.then((r) => {
|
|
6252
|
+
if (r.active)
|
|
6253
|
+
r.active.postMessage(cmd.data);
|
|
6254
|
+
});
|
|
6255
|
+
}
|
|
6256
|
+
else if (cmd.action === 'unregister') {
|
|
6257
|
+
navigator.serviceWorker.ready.then((r) => r.unregister());
|
|
6258
|
+
}
|
|
6259
|
+
},
|
|
6260
|
+
error: (err) => console.error('[SW driver] Error in sink stream:', err),
|
|
6261
|
+
});
|
|
6262
|
+
}
|
|
6263
|
+
return {
|
|
6264
|
+
select(type) {
|
|
6265
|
+
let cb;
|
|
6266
|
+
const in$ = xs$1.create({
|
|
6267
|
+
start: (listener) => {
|
|
6268
|
+
cb = ({ detail }) => {
|
|
6269
|
+
if (!type || detail.type === type)
|
|
6270
|
+
listener.next(detail.data);
|
|
6271
|
+
};
|
|
6272
|
+
events.addEventListener('data', cb);
|
|
6273
|
+
},
|
|
6274
|
+
stop: () => {
|
|
6275
|
+
if (cb)
|
|
6276
|
+
events.removeEventListener('data', cb);
|
|
6277
|
+
},
|
|
6278
|
+
});
|
|
6279
|
+
return adapt(in$);
|
|
6280
|
+
},
|
|
6281
|
+
};
|
|
6282
|
+
};
|
|
6283
|
+
}
|
|
6284
|
+
// ── onlineStatus$ ────────────────────────────────────────────
|
|
6285
|
+
function onlineStatus$() {
|
|
6286
|
+
if (typeof window === 'undefined') {
|
|
6287
|
+
return xs$1.of(true);
|
|
6288
|
+
}
|
|
6289
|
+
let cleanup;
|
|
6290
|
+
return xs$1.create({
|
|
6291
|
+
start(listener) {
|
|
6292
|
+
listener.next(navigator.onLine);
|
|
6293
|
+
const on = () => listener.next(true);
|
|
6294
|
+
const off = () => listener.next(false);
|
|
6295
|
+
window.addEventListener('online', on);
|
|
6296
|
+
window.addEventListener('offline', off);
|
|
6297
|
+
cleanup = () => {
|
|
6298
|
+
window.removeEventListener('online', on);
|
|
6299
|
+
window.removeEventListener('offline', off);
|
|
6300
|
+
};
|
|
6301
|
+
},
|
|
6302
|
+
stop() {
|
|
6303
|
+
cleanup?.();
|
|
6304
|
+
cleanup = undefined;
|
|
6305
|
+
},
|
|
6306
|
+
});
|
|
6307
|
+
}
|
|
6308
|
+
// ── createInstallPrompt ──────────────────────────────────────
|
|
6309
|
+
function createInstallPrompt() {
|
|
6310
|
+
let deferredPrompt = null;
|
|
6311
|
+
const events = new EventTarget();
|
|
6312
|
+
if (typeof window !== 'undefined') {
|
|
6313
|
+
window.addEventListener('beforeinstallprompt', (e) => {
|
|
6314
|
+
e.preventDefault();
|
|
6315
|
+
deferredPrompt = e;
|
|
6316
|
+
events.dispatchEvent(new CustomEvent('data', { detail: { type: 'beforeinstallprompt', data: true } }));
|
|
6317
|
+
});
|
|
6318
|
+
window.addEventListener('appinstalled', () => {
|
|
6319
|
+
deferredPrompt = null;
|
|
6320
|
+
events.dispatchEvent(new CustomEvent('data', { detail: { type: 'appinstalled', data: true } }));
|
|
6321
|
+
});
|
|
6322
|
+
}
|
|
6323
|
+
return {
|
|
6324
|
+
select(type) {
|
|
6325
|
+
let cb;
|
|
6326
|
+
const in$ = xs$1.create({
|
|
6327
|
+
start: (listener) => {
|
|
6328
|
+
cb = ({ detail }) => {
|
|
6329
|
+
if (detail.type === type)
|
|
6330
|
+
listener.next(detail.data);
|
|
6331
|
+
};
|
|
6332
|
+
events.addEventListener('data', cb);
|
|
6333
|
+
},
|
|
6334
|
+
stop: () => {
|
|
6335
|
+
if (cb)
|
|
6336
|
+
events.removeEventListener('data', cb);
|
|
6337
|
+
},
|
|
6338
|
+
});
|
|
6339
|
+
return adapt(in$);
|
|
6340
|
+
},
|
|
6341
|
+
prompt() {
|
|
6342
|
+
return deferredPrompt?.prompt();
|
|
6343
|
+
},
|
|
6344
|
+
};
|
|
6345
|
+
}
|
|
6346
|
+
|
|
5845
6347
|
/**
|
|
5846
6348
|
* Server-Side Rendering utilities for Sygnal components.
|
|
5847
6349
|
*
|
|
@@ -6012,6 +6514,24 @@ function processSSRTree(vnode, context, parentState) {
|
|
|
6012
6514
|
key: undefined,
|
|
6013
6515
|
};
|
|
6014
6516
|
}
|
|
6517
|
+
// ClientOnly: render fallback during SSR, skip children (they need a browser)
|
|
6518
|
+
if (sel === 'clientonly') {
|
|
6519
|
+
const props = vnode.data?.props || {};
|
|
6520
|
+
const fallback = props.fallback;
|
|
6521
|
+
if (fallback) {
|
|
6522
|
+
// fallback can be a VNode or a string
|
|
6523
|
+
return processSSRTree(fallback, context, parentState);
|
|
6524
|
+
}
|
|
6525
|
+
// No fallback — render an empty placeholder div
|
|
6526
|
+
return {
|
|
6527
|
+
sel: 'div',
|
|
6528
|
+
data: { attrs: { 'data-sygnal-clientonly': '' } },
|
|
6529
|
+
children: [],
|
|
6530
|
+
text: undefined,
|
|
6531
|
+
elm: undefined,
|
|
6532
|
+
key: undefined,
|
|
6533
|
+
};
|
|
6534
|
+
}
|
|
6015
6535
|
// Slot: unwrap to children
|
|
6016
6536
|
if (sel === 'slot') {
|
|
6017
6537
|
const children = vnode.children || [];
|
|
@@ -6514,22 +7034,28 @@ exports.collection = collection;
|
|
|
6514
7034
|
exports.component = component;
|
|
6515
7035
|
exports.createCommand = createCommand;
|
|
6516
7036
|
exports.createElement = createElement;
|
|
7037
|
+
exports.createInstallPrompt = createInstallPrompt;
|
|
6517
7038
|
exports.createRef = createRef;
|
|
6518
7039
|
exports.createRef$ = createRef$;
|
|
6519
7040
|
exports.driverFromAsync = driverFromAsync;
|
|
7041
|
+
exports.emit = emit;
|
|
6520
7042
|
exports.enableHMR = enableHMR;
|
|
6521
7043
|
exports.exactState = exactState;
|
|
6522
7044
|
exports.getDevTools = getDevTools;
|
|
6523
7045
|
exports.lazy = lazy;
|
|
6524
7046
|
exports.makeDOMDriver = makeDOMDriver;
|
|
6525
7047
|
exports.makeDragDriver = makeDragDriver;
|
|
7048
|
+
exports.makeServiceWorkerDriver = makeServiceWorkerDriver;
|
|
6526
7049
|
exports.mockDOMSource = mockDOMSource;
|
|
7050
|
+
exports.onlineStatus$ = onlineStatus$;
|
|
6527
7051
|
exports.portal = Portal;
|
|
6528
7052
|
exports.processDrag = processDrag;
|
|
6529
7053
|
exports.processForm = processForm;
|
|
6530
7054
|
exports.renderComponent = renderComponent;
|
|
6531
7055
|
exports.renderToString = renderToString;
|
|
6532
7056
|
exports.run = run;
|
|
7057
|
+
exports.set = set;
|
|
6533
7058
|
exports.switchable = switchable;
|
|
6534
7059
|
exports.thunk = thunk;
|
|
7060
|
+
exports.toggle = toggle;
|
|
6535
7061
|
exports.xs = xs;
|