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/src/extra/pwa.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import xs, {Stream} from 'xstream';
|
|
2
|
+
import {adapt} from '../cycle/run/adapt';
|
|
3
|
+
|
|
4
|
+
// ── Types ────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface ServiceWorkerSource {
|
|
7
|
+
select(type?: string): any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ServiceWorkerCommand {
|
|
11
|
+
action: 'skipWaiting' | 'postMessage' | 'unregister';
|
|
12
|
+
data?: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ServiceWorkerOptions {
|
|
16
|
+
scope?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface InstallPrompt {
|
|
20
|
+
select(type: 'beforeinstallprompt' | 'appinstalled'): any;
|
|
21
|
+
prompt(): Promise<any> | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── makeServiceWorkerDriver ──────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function trackWorker(worker: ServiceWorker, events: EventTarget) {
|
|
27
|
+
const emit = (type: string, data: any) =>
|
|
28
|
+
events.dispatchEvent(new CustomEvent('data', {detail: {type, data}}));
|
|
29
|
+
|
|
30
|
+
worker.addEventListener('statechange', () => {
|
|
31
|
+
if (worker.state === 'installed') emit('installed', true);
|
|
32
|
+
if (worker.state === 'activated') emit('activated', true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (worker.state === 'installed') emit('waiting', worker);
|
|
36
|
+
if (worker.state === 'activated') emit('activated', true);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function makeServiceWorkerDriver(
|
|
40
|
+
scriptUrl: string,
|
|
41
|
+
options: ServiceWorkerOptions = {},
|
|
42
|
+
): (sink$: Stream<ServiceWorkerCommand>) => ServiceWorkerSource {
|
|
43
|
+
return function serviceWorkerDriver(sink$: Stream<ServiceWorkerCommand>): ServiceWorkerSource {
|
|
44
|
+
const events = new EventTarget();
|
|
45
|
+
|
|
46
|
+
if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
|
|
47
|
+
navigator.serviceWorker
|
|
48
|
+
.register(scriptUrl, {scope: options.scope})
|
|
49
|
+
.then((reg) => {
|
|
50
|
+
const emit = (type: string, data: any) =>
|
|
51
|
+
events.dispatchEvent(new CustomEvent('data', {detail: {type, data}}));
|
|
52
|
+
|
|
53
|
+
if (reg.installing) trackWorker(reg.installing, events);
|
|
54
|
+
if (reg.waiting) emit('waiting', reg.waiting);
|
|
55
|
+
if (reg.active) emit('activated', true);
|
|
56
|
+
|
|
57
|
+
reg.addEventListener('updatefound', () => {
|
|
58
|
+
if (reg.installing) trackWorker(reg.installing, events);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
62
|
+
emit('controlling', true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
navigator.serviceWorker.addEventListener('message', (e) => {
|
|
66
|
+
emit('message', (e as MessageEvent).data);
|
|
67
|
+
});
|
|
68
|
+
})
|
|
69
|
+
.catch((err) => {
|
|
70
|
+
events.dispatchEvent(new CustomEvent('data', {detail: {type: 'error', data: err}}));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
sink$.addListener({
|
|
74
|
+
next: (cmd: ServiceWorkerCommand) => {
|
|
75
|
+
if (cmd.action === 'skipWaiting') {
|
|
76
|
+
navigator.serviceWorker.ready.then((r) => {
|
|
77
|
+
if (r.waiting) r.waiting.postMessage({type: 'SKIP_WAITING'});
|
|
78
|
+
});
|
|
79
|
+
} else if (cmd.action === 'postMessage') {
|
|
80
|
+
navigator.serviceWorker.ready.then((r) => {
|
|
81
|
+
if (r.active) r.active.postMessage(cmd.data);
|
|
82
|
+
});
|
|
83
|
+
} else if (cmd.action === 'unregister') {
|
|
84
|
+
navigator.serviceWorker.ready.then((r) => r.unregister());
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
error: (err: any) => console.error('[SW driver] Error in sink stream:', err),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
select(type?: string) {
|
|
93
|
+
let cb: ((e: Event) => void) | undefined;
|
|
94
|
+
const in$ = xs.create<any>({
|
|
95
|
+
start: (listener) => {
|
|
96
|
+
cb = ({detail}: any) => {
|
|
97
|
+
if (!type || detail.type === type) listener.next(detail.data);
|
|
98
|
+
};
|
|
99
|
+
events.addEventListener('data', cb);
|
|
100
|
+
},
|
|
101
|
+
stop: () => {
|
|
102
|
+
if (cb) events.removeEventListener('data', cb);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
return adapt(in$);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── onlineStatus$ ────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export function onlineStatus$(): Stream<boolean> {
|
|
114
|
+
if (typeof window === 'undefined') {
|
|
115
|
+
return xs.of(true);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let cleanup: (() => void) | undefined;
|
|
119
|
+
|
|
120
|
+
return xs.create<boolean>({
|
|
121
|
+
start(listener) {
|
|
122
|
+
listener.next(navigator.onLine);
|
|
123
|
+
const on = () => listener.next(true);
|
|
124
|
+
const off = () => listener.next(false);
|
|
125
|
+
window.addEventListener('online', on);
|
|
126
|
+
window.addEventListener('offline', off);
|
|
127
|
+
cleanup = () => {
|
|
128
|
+
window.removeEventListener('online', on);
|
|
129
|
+
window.removeEventListener('offline', off);
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
stop() {
|
|
133
|
+
cleanup?.();
|
|
134
|
+
cleanup = undefined;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── createInstallPrompt ──────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
export function createInstallPrompt(): InstallPrompt {
|
|
142
|
+
let deferredPrompt: any = null;
|
|
143
|
+
const events = new EventTarget();
|
|
144
|
+
|
|
145
|
+
if (typeof window !== 'undefined') {
|
|
146
|
+
window.addEventListener('beforeinstallprompt', (e) => {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
deferredPrompt = e;
|
|
149
|
+
events.dispatchEvent(new CustomEvent('data', {detail: {type: 'beforeinstallprompt', data: true}}));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
window.addEventListener('appinstalled', () => {
|
|
153
|
+
deferredPrompt = null;
|
|
154
|
+
events.dispatchEvent(new CustomEvent('data', {detail: {type: 'appinstalled', data: true}}));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
select(type: 'beforeinstallprompt' | 'appinstalled') {
|
|
160
|
+
let cb: ((e: Event) => void) | undefined;
|
|
161
|
+
const in$ = xs.create<any>({
|
|
162
|
+
start: (listener) => {
|
|
163
|
+
cb = ({detail}: any) => {
|
|
164
|
+
if (detail.type === type) listener.next(detail.data);
|
|
165
|
+
};
|
|
166
|
+
events.addEventListener('data', cb);
|
|
167
|
+
},
|
|
168
|
+
stop: () => {
|
|
169
|
+
if (cb) events.removeEventListener('data', cb);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
return adapt(in$);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
prompt() {
|
|
176
|
+
return deferredPrompt?.prompt();
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reducer helpers for common state update patterns.
|
|
3
|
+
*
|
|
4
|
+
* These reduce boilerplate in model definitions by providing
|
|
5
|
+
* shorthand factories for the most frequent reducer shapes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── set() ──────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Create a reducer that merges a partial update into state.
|
|
11
|
+
*
|
|
12
|
+
* Static form — merge a fixed object:
|
|
13
|
+
* set({ isEditing: true })
|
|
14
|
+
*
|
|
15
|
+
* Dynamic form — function receives (state, data, next, props) and
|
|
16
|
+
* returns the partial update to merge:
|
|
17
|
+
* set((state, title) => ({ title }))
|
|
18
|
+
*/
|
|
19
|
+
export function set<S = any>(
|
|
20
|
+
partial: Partial<S> | ((state: S, data: any, next: Function, props: any) => Partial<S>)
|
|
21
|
+
): (state: S, data: any, next: Function, props: any) => S {
|
|
22
|
+
if (typeof partial === 'function') {
|
|
23
|
+
return (state, data, next, props) => ({
|
|
24
|
+
...state,
|
|
25
|
+
...partial(state, data, next, props),
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
return (state) => ({ ...state, ...partial })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── toggle() ───────────────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Create a reducer that toggles a boolean field on state.
|
|
34
|
+
*
|
|
35
|
+
* toggle('showModal')
|
|
36
|
+
* // equivalent to: (state) => ({ ...state, showModal: !state.showModal })
|
|
37
|
+
*/
|
|
38
|
+
export function toggle<S = any>(field: keyof S & string): (state: S) => S {
|
|
39
|
+
return (state) => ({ ...state, [field]: !state[field] })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── emit() ─────────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Create a model entry that emits an EVENTS bus event.
|
|
45
|
+
*
|
|
46
|
+
* With static data:
|
|
47
|
+
* emit('DELETE_LANE', { laneId: 42 })
|
|
48
|
+
*
|
|
49
|
+
* With dynamic data derived from state:
|
|
50
|
+
* emit('DELETE_LANE', (state) => ({ laneId: state.id }))
|
|
51
|
+
*
|
|
52
|
+
* Fire-and-forget (no data):
|
|
53
|
+
* emit('REFRESH')
|
|
54
|
+
*/
|
|
55
|
+
export function emit(
|
|
56
|
+
type: string,
|
|
57
|
+
data?: any | ((state: any, actionData: any, next: Function, props: any) => any)
|
|
58
|
+
): { EVENTS: (state: any, actionData: any, next: Function, props: any) => { type: string; data: any } } {
|
|
59
|
+
return {
|
|
60
|
+
EVENTS: typeof data === 'function'
|
|
61
|
+
? (state, actionData, next, props) => ({ type, data: data(state, actionData, next, props) })
|
|
62
|
+
: () => ({ type, data }),
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/extra/ssr.ts
CHANGED
|
@@ -193,6 +193,25 @@ function processSSRTree(vnode: any, context: Record<string, any>, parentState?:
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// ClientOnly: render fallback during SSR, skip children (they need a browser)
|
|
197
|
+
if (sel === 'clientonly') {
|
|
198
|
+
const props = vnode.data?.props || {}
|
|
199
|
+
const fallback = props.fallback
|
|
200
|
+
if (fallback) {
|
|
201
|
+
// fallback can be a VNode or a string
|
|
202
|
+
return processSSRTree(fallback, context, parentState)
|
|
203
|
+
}
|
|
204
|
+
// No fallback — render an empty placeholder div
|
|
205
|
+
return {
|
|
206
|
+
sel: 'div',
|
|
207
|
+
data: {attrs: {'data-sygnal-clientonly': ''}},
|
|
208
|
+
children: [],
|
|
209
|
+
text: undefined,
|
|
210
|
+
elm: undefined,
|
|
211
|
+
key: undefined,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
196
215
|
// Slot: unwrap to children
|
|
197
216
|
if (sel === 'slot') {
|
|
198
217
|
const children = vnode.children || []
|
package/src/index.d.ts
CHANGED
|
@@ -378,6 +378,40 @@ export function enableHMR<STATE = any, DRIVERS = {}>(
|
|
|
378
378
|
export function classes(...classes: ClassesType): string
|
|
379
379
|
export function exactState<STATE>(): <ACTUAL extends STATE>(state: ExactShape<STATE, ACTUAL>) => STATE
|
|
380
380
|
|
|
381
|
+
// ── Reducer helpers ────────────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Create a reducer that merges a partial update into state.
|
|
385
|
+
*
|
|
386
|
+
* Static form — merge a fixed object:
|
|
387
|
+
* `set({ isEditing: true })`
|
|
388
|
+
*
|
|
389
|
+
* Dynamic form — function receives (state, data, next, props) and
|
|
390
|
+
* returns the partial update to merge:
|
|
391
|
+
* `set((state, title) => ({ title }))`
|
|
392
|
+
*/
|
|
393
|
+
export function set<S = any>(
|
|
394
|
+
partial: Partial<S> | ((state: S, data: any, next: Function, props: any) => Partial<S>)
|
|
395
|
+
): (state: S, data: any, next: Function, props: any) => S
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Create a reducer that toggles a boolean field on state.
|
|
399
|
+
*
|
|
400
|
+
* `toggle('showModal')`
|
|
401
|
+
*/
|
|
402
|
+
export function toggle<S = any>(field: keyof S & string): (state: S) => S
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Create a model entry that emits an EVENTS bus event.
|
|
406
|
+
*
|
|
407
|
+
* `emit('DELETE_LANE', (state) => ({ laneId: state.id }))`
|
|
408
|
+
* `emit('REFRESH')`
|
|
409
|
+
*/
|
|
410
|
+
export function emit(
|
|
411
|
+
type: string,
|
|
412
|
+
data?: any | ((state: any, actionData: any, next: Function, props: any) => any)
|
|
413
|
+
): { EVENTS: (state: any, actionData: any, next: Function, props: any) => { type: string; data: any } }
|
|
414
|
+
|
|
381
415
|
/**
|
|
382
416
|
* Any object with an events() method (e.g., DOM.select('form')).
|
|
383
417
|
* Uses permissive signature to be compatible with MainDOMSource's overloaded events().
|
|
@@ -561,6 +595,35 @@ export interface CommandSource {
|
|
|
561
595
|
|
|
562
596
|
export function createCommand(): Command
|
|
563
597
|
|
|
598
|
+
// ── PWA Helpers ──────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
export interface ServiceWorkerSource {
|
|
601
|
+
select(type?: 'installed' | 'activated' | 'waiting' | 'controlling' | 'error' | 'message' | string): Stream<any>;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export interface ServiceWorkerCommand {
|
|
605
|
+
action: 'skipWaiting' | 'postMessage' | 'unregister';
|
|
606
|
+
data?: any;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export interface ServiceWorkerOptions {
|
|
610
|
+
scope?: string;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function makeServiceWorkerDriver(
|
|
614
|
+
scriptUrl: string,
|
|
615
|
+
options?: ServiceWorkerOptions
|
|
616
|
+
): (sink$: Stream<ServiceWorkerCommand>) => ServiceWorkerSource
|
|
617
|
+
|
|
618
|
+
export function onlineStatus$(): Stream<boolean>
|
|
619
|
+
|
|
620
|
+
export interface InstallPrompt {
|
|
621
|
+
select(type: 'beforeinstallprompt' | 'appinstalled'): Stream<any>;
|
|
622
|
+
prompt(): Promise<any> | undefined;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function createInstallPrompt(): InstallPrompt
|
|
626
|
+
|
|
564
627
|
export interface RenderOptions {
|
|
565
628
|
/** Override initial state (defaults to component's .initialState) */
|
|
566
629
|
initialState?: any;
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ export { createElement } from './pragma/index'
|
|
|
21
21
|
export { createCommand } from './extra/command'
|
|
22
22
|
export { createRef, createRef$ } from './extra/ref'
|
|
23
23
|
export { renderComponent } from './extra/testing'
|
|
24
|
+
export { set, toggle, emit } from './extra/reducers'
|
|
25
|
+
export { makeServiceWorkerDriver, onlineStatus$, createInstallPrompt } from './extra/pwa'
|
|
24
26
|
export { renderToString } from './extra/ssr'
|
|
25
27
|
export { default as xs } from './extra/xstreamCompat'
|
|
26
28
|
export { getDevTools } from './extra/devtools'
|
package/src/vike/+config.ts
CHANGED
|
@@ -15,13 +15,17 @@ export default {
|
|
|
15
15
|
onRenderHtml: 'import:sygnal/vike/onRenderHtml:onRenderHtml',
|
|
16
16
|
onRenderClient: 'import:sygnal/vike/onRenderClient:onRenderClient',
|
|
17
17
|
|
|
18
|
-
passToClient: ['data', 'routeParams'],
|
|
18
|
+
passToClient: ['data', 'routeParams', 'urlPathname'],
|
|
19
19
|
|
|
20
20
|
meta: {
|
|
21
21
|
Layout: {
|
|
22
22
|
env: { server: true, client: true },
|
|
23
23
|
cumulative: true,
|
|
24
24
|
},
|
|
25
|
+
Wrapper: {
|
|
26
|
+
env: { server: true, client: true },
|
|
27
|
+
cumulative: true,
|
|
28
|
+
},
|
|
25
29
|
Head: {
|
|
26
30
|
env: { server: true },
|
|
27
31
|
},
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {h} from '../cycle/dom/snabbdom';
|
|
2
|
+
|
|
3
|
+
const ClientOnly = (props: any) => {
|
|
4
|
+
const {children, ...sanitizedProps} = props;
|
|
5
|
+
return h('clientonly', {props: sanitizedProps}, children);
|
|
6
|
+
};
|
|
7
|
+
(ClientOnly as any).label = 'clientonly';
|
|
8
|
+
(ClientOnly as any).preventInstantiation = true;
|
|
9
|
+
|
|
10
|
+
export {ClientOnly};
|