sygnal 5.2.0 → 5.2.1

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.
@@ -10,7 +10,7 @@
10
10
  * export default defineConfig({ plugins: [sygnal()] })
11
11
  *
12
12
  * What it does:
13
- * 1. Configures esbuild for automatic JSX transform with sygnal as the import source
13
+ * 1. Configures OXC for automatic JSX transform with sygnal as the import source
14
14
  * 2. Detects files that call `run()` from sygnal and auto-injects HMR wiring
15
15
  *
16
16
  * The HMR transform finds the pattern:
@@ -34,9 +34,11 @@ function sygnal(options = {}) {
34
34
  if (disableJsx)
35
35
  return;
36
36
  return {
37
- esbuild: {
38
- jsx: 'automatic',
39
- jsxImportSource: 'sygnal',
37
+ oxc: {
38
+ jsx: {
39
+ runtime: 'automatic',
40
+ importSource: 'sygnal',
41
+ },
40
42
  },
41
43
  };
42
44
  },
@@ -8,7 +8,7 @@
8
8
  * export default defineConfig({ plugins: [sygnal()] })
9
9
  *
10
10
  * What it does:
11
- * 1. Configures esbuild for automatic JSX transform with sygnal as the import source
11
+ * 1. Configures OXC for automatic JSX transform with sygnal as the import source
12
12
  * 2. Detects files that call `run()` from sygnal and auto-injects HMR wiring
13
13
  *
14
14
  * The HMR transform finds the pattern:
@@ -32,9 +32,11 @@ function sygnal(options = {}) {
32
32
  if (disableJsx)
33
33
  return;
34
34
  return {
35
- esbuild: {
36
- jsx: 'automatic',
37
- jsxImportSource: 'sygnal',
35
+ oxc: {
36
+ jsx: {
37
+ runtime: 'automatic',
38
+ importSource: 'sygnal',
39
+ },
38
40
  },
39
41
  };
40
42
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sygnal",
3
- "version": "5.2.0",
3
+ "version": "5.2.1",
4
4
  "description": "An intuitive framework for building fast and small components or applications based on Cycle.js",
5
5
  "main": "./dist/index.cjs.js",
6
6
  "types": "./dist/index.d.ts",
@@ -69,6 +69,10 @@
69
69
  "import": "./dist/vike/onRenderClient.mjs",
70
70
  "require": "./dist/vike/onRenderClient.cjs.js"
71
71
  },
72
+ "./vike/ClientOnly": {
73
+ "import": "./dist/vike/ClientOnly.mjs",
74
+ "require": "./dist/vike/ClientOnly.cjs.js"
75
+ },
72
76
  "./types": {
73
77
  "import": "./dist/index.d.ts",
74
78
  "require": "./dist/index.d.ts"
package/src/component.ts CHANGED
@@ -91,6 +91,7 @@ export interface ComponentOptions {
91
91
  stateSourceName?: string;
92
92
  requestSourceName?: string;
93
93
  isolateOpts?: string | boolean | Record<string, any>;
94
+ isolatedState?: boolean;
94
95
  onError?: (error: Error, info: { componentName: string }) => any;
95
96
  debug?: boolean;
96
97
  }
@@ -169,6 +170,7 @@ class Component {
169
170
  sourceNames: string[];
170
171
  _debug: boolean;
171
172
  onError: ((error: Error, info: { componentName: string }) => any) | undefined;
173
+ isolatedState: boolean;
172
174
  isSubComponent: boolean;
173
175
  currentState: any;
174
176
  currentProps: any;
@@ -202,7 +204,7 @@ class Component {
202
204
  _readyChanged$: any;
203
205
  _readyChangedListener: any;
204
206
 
205
- 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}: ComponentOptions) {
207
+ 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}: ComponentOptions) {
206
208
  if (!sources || !isObj(sources)) throw new Error(`[${name}] Missing or invalid sources`)
207
209
 
208
210
  this._componentNumber = COMPONENT_COUNT++
@@ -225,6 +227,7 @@ class Component {
225
227
  this.requestSourceName = requestSourceName
226
228
  this.sourceNames = Object.keys(sources)
227
229
  this.onError = onError
230
+ this.isolatedState = isolatedState
228
231
  this._debug = debug
229
232
 
230
233
  // Warn if calculated fields shadow base state keys
@@ -649,7 +652,7 @@ class Component {
649
652
  const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE
650
653
  const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState
651
654
  const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState }
652
- if (this.isSubComponent && this.initialState) {
655
+ if (this.isSubComponent && this.initialState && !this.isolatedState) {
653
656
  console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`)
654
657
  }
655
658
  const hasInitialState = (typeof effectiveInitialState !== 'undefined')
@@ -851,6 +854,7 @@ class Component {
851
854
  .map((vdom: any) => processLazy(vdom, this))
852
855
  .map(processPortals)
853
856
  .map(processTransitions)
857
+ .map(processClientOnly)
854
858
  .compose(this.instantiateSubComponents.bind(this))
855
859
  .filter((val: any) => val !== undefined)
856
860
  .compose(this.renderVdom.bind(this))
@@ -1876,6 +1880,29 @@ function processTransitions(vnode: any): any {
1876
1880
  return vnode
1877
1881
  }
1878
1882
 
1883
+ function processClientOnly(vnode: any): any {
1884
+ if (!vnode || !vnode.sel) return vnode
1885
+ if (vnode.sel === 'clientonly') {
1886
+ // On the client, unwrap to children (render them normally)
1887
+ const children = vnode.children || []
1888
+ if (children.length === 0) return { sel: 'div', data: {}, children: [] }
1889
+ if (children.length === 1) return processClientOnly(children[0])
1890
+ // Multiple children: wrap in a div
1891
+ return {
1892
+ sel: 'div',
1893
+ data: {},
1894
+ children: children.map(processClientOnly),
1895
+ text: undefined,
1896
+ elm: undefined,
1897
+ key: undefined,
1898
+ }
1899
+ }
1900
+ if (vnode.children && vnode.children.length > 0) {
1901
+ vnode.children = vnode.children.map(processClientOnly)
1902
+ }
1903
+ return vnode
1904
+ }
1905
+
1879
1906
  function applyTransitionHooks(vnode: any, name: string, duration?: number): any {
1880
1907
  const existingInsert = vnode.data?.hook?.insert
1881
1908
  const existingRemove = vnode.data?.hook?.remove
@@ -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().
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ 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'
24
25
  export { renderToString } from './extra/ssr'
25
26
  export { default as xs } from './extra/xstreamCompat'
26
27
  export { getDevTools } from './extra/devtools'
@@ -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};