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.
@@ -14,12 +14,16 @@ var _config = {
14
14
  hydrationCanBeAborted: true,
15
15
  onRenderHtml: 'import:sygnal/vike/onRenderHtml:onRenderHtml',
16
16
  onRenderClient: 'import:sygnal/vike/onRenderClient:onRenderClient',
17
- passToClient: ['data', 'routeParams'],
17
+ passToClient: ['data', 'routeParams', 'urlPathname'],
18
18
  meta: {
19
19
  Layout: {
20
20
  env: { server: true, client: true },
21
21
  cumulative: true,
22
22
  },
23
+ Wrapper: {
24
+ env: { server: true, client: true },
25
+ cumulative: true,
26
+ },
23
27
  Head: {
24
28
  env: { server: true },
25
29
  },
@@ -12,12 +12,16 @@ var _config = {
12
12
  hydrationCanBeAborted: true,
13
13
  onRenderHtml: 'import:sygnal/vike/onRenderHtml:onRenderHtml',
14
14
  onRenderClient: 'import:sygnal/vike/onRenderClient:onRenderClient',
15
- passToClient: ['data', 'routeParams'],
15
+ passToClient: ['data', 'routeParams', 'urlPathname'],
16
16
  meta: {
17
17
  Layout: {
18
18
  env: { server: true, client: true },
19
19
  cumulative: true,
20
20
  },
21
+ Wrapper: {
22
+ env: { server: true, client: true },
23
+ cumulative: true,
24
+ },
21
25
  Head: {
22
26
  env: { server: true },
23
27
  },
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ var h_js = require('snabbdom/build/h.js');
4
+ require('snabbdom/build/init.js');
5
+ require('snabbdom/build/tovnode.js');
6
+ require('snabbdom/build/vnode.js');
7
+ var jsx_js = require('snabbdom/build/jsx.js');
8
+ require('snabbdom/build/modules/class.js');
9
+ require('snabbdom/build/modules/props.js');
10
+ require('snabbdom/build/modules/attributes.js');
11
+ require('snabbdom/build/modules/dataset.js');
12
+
13
+ /**
14
+ * Local snabbdom re-export barrel.
15
+ *
16
+ * Imports from specific snabbdom subpaths instead of the main barrel to avoid
17
+ * triggering the broken `styleModule` top-level `window` reference in
18
+ * snabbdom 3.6.3 (which causes ReferenceError in Node.js environments).
19
+ *
20
+ * The styleModule is provided separately by ./styleModule.ts with a fixed
21
+ * window guard.
22
+ */
23
+ // Core
24
+ // Tag Fragment so we can identify it even after minification mangles Function.name
25
+ jsx_js.Fragment.__sygnalFragment = true;
26
+
27
+ const ClientOnly = (props) => {
28
+ const { children, ...sanitizedProps } = props;
29
+ return h_js.h('clientonly', { props: sanitizedProps }, children);
30
+ };
31
+ ClientOnly.label = 'clientonly';
32
+ ClientOnly.preventInstantiation = true;
33
+
34
+ exports.ClientOnly = ClientOnly;
@@ -0,0 +1,32 @@
1
+ import { h } from 'snabbdom/build/h.js';
2
+ import 'snabbdom/build/init.js';
3
+ import 'snabbdom/build/tovnode.js';
4
+ import 'snabbdom/build/vnode.js';
5
+ import { Fragment } from 'snabbdom/build/jsx.js';
6
+ import 'snabbdom/build/modules/class.js';
7
+ import 'snabbdom/build/modules/props.js';
8
+ import 'snabbdom/build/modules/attributes.js';
9
+ import 'snabbdom/build/modules/dataset.js';
10
+
11
+ /**
12
+ * Local snabbdom re-export barrel.
13
+ *
14
+ * Imports from specific snabbdom subpaths instead of the main barrel to avoid
15
+ * triggering the broken `styleModule` top-level `window` reference in
16
+ * snabbdom 3.6.3 (which causes ReferenceError in Node.js environments).
17
+ *
18
+ * The styleModule is provided separately by ./styleModule.ts with a fixed
19
+ * window guard.
20
+ */
21
+ // Core
22
+ // Tag Fragment so we can identify it even after minification mangles Function.name
23
+ Fragment.__sygnalFragment = true;
24
+
25
+ const ClientOnly = (props) => {
26
+ const { children, ...sanitizedProps } = props;
27
+ return h('clientonly', { props: sanitizedProps }, children);
28
+ };
29
+ ClientOnly.label = 'clientonly';
30
+ ClientOnly.preventInstantiation = true;
31
+
32
+ export { ClientOnly };
@@ -8,53 +8,310 @@ var sygnal = require('sygnal');
8
8
  * On first page load (hydration): reads server-serialized state and
9
9
  * boots the Sygnal app via run().
10
10
  *
11
- * On client-side navigation: disposes the previous app and boots
12
- * a fresh one for the new page.
11
+ * On client-side navigation with a Layout/Wrapper: the Layout and Wrapper
12
+ * stay mounted and only the Page sub-component is hot-swapped. Layout and
13
+ * Wrapper state persists across navigations. Without either, the app is
14
+ * disposed and recreated.
13
15
  *
14
- * Layout is rendered outside #page-view by onRenderHtml, so it persists
15
- * across navigations without being destroyed by Sygnal's DOM driver.
16
+ * Nesting order: Wrapper > Layout > Page
17
+ * - Wrappers are outermost (context providers, state management)
18
+ * - Layouts are inside wrappers (visual structure, navigation chrome)
19
+ * - Page is innermost (route-specific content, swapped on navigation)
16
20
  */
17
21
  // Import from the package entry so rollup externalizes it
18
22
  // @ts-ignore — resolved at runtime via package exports
19
23
  /** Track the running Sygnal app for disposal on navigation */
20
24
  let currentApp = null;
25
+ /**
26
+ * Mutable references read by the wrapper's view function.
27
+ * Updated on navigation to swap the Page without disposing the Layout.
28
+ */
29
+ let currentPage = null;
30
+ let currentPageName = '';
31
+ let currentPageData = {};
32
+ let currentRouteParams = {};
33
+ let currentUrlPathname = '';
34
+ let pageNavCounter = 0;
35
+ /**
36
+ * Build a component vnode that matches what the JSX pragma produces.
37
+ */
38
+ function componentVNode(comp, stateField, children, compInitialState) {
39
+ const name = comp.componentName || comp.name || 'FUNCTION_COMPONENT';
40
+ return {
41
+ sel: name,
42
+ data: {
43
+ props: {
44
+ state: stateField,
45
+ sygnalOptions: {
46
+ name,
47
+ view: comp,
48
+ model: comp.model,
49
+ intent: comp.intent,
50
+ hmrActions: comp.hmrActions,
51
+ context: comp.context,
52
+ peers: comp.peers,
53
+ components: comp.components,
54
+ initialState: compInitialState,
55
+ isolatedState: true,
56
+ calculated: comp.calculated,
57
+ storeCalculatedInState: comp.storeCalculatedInState,
58
+ DOMSourceName: comp.DOMSourceName,
59
+ stateSourceName: comp.stateSourceName,
60
+ onError: comp.onError,
61
+ debug: comp.debug,
62
+ },
63
+ },
64
+ },
65
+ children,
66
+ text: undefined,
67
+ elm: undefined,
68
+ key: '__vike_' + stateField + '__',
69
+ };
70
+ }
71
+ /**
72
+ * Build a vnode for the current Page as a child of the Layout.
73
+ * Reads from mutable `currentPage` / `currentPageName` so the wrapper
74
+ * view picks up the new Page component on navigation without being recreated.
75
+ */
76
+ function pageChildVNode(pageState) {
77
+ // Include pageNavCounter in sel so instantiateSubComponents detects a
78
+ // component swap even when both pages have the same function name (e.g. 'Page').
79
+ const sel = currentPageName + '__nav' + pageNavCounter;
80
+ return {
81
+ sel,
82
+ data: {
83
+ props: {
84
+ state: 'page',
85
+ sygnalOptions: {
86
+ name: sel,
87
+ view: currentPage,
88
+ model: currentPage.model,
89
+ intent: currentPage.intent,
90
+ hmrActions: currentPage.hmrActions,
91
+ context: currentPage.context,
92
+ peers: currentPage.peers,
93
+ components: currentPage.components,
94
+ initialState: pageState,
95
+ isolatedState: true,
96
+ calculated: currentPage.calculated,
97
+ storeCalculatedInState: currentPage.storeCalculatedInState,
98
+ DOMSourceName: currentPage.DOMSourceName,
99
+ stateSourceName: currentPage.stateSourceName,
100
+ onError: currentPage.onError,
101
+ debug: currentPage.debug,
102
+ },
103
+ },
104
+ },
105
+ children: [],
106
+ text: undefined,
107
+ elm: undefined,
108
+ key: '__vike_page__',
109
+ };
110
+ }
111
+ /**
112
+ * Create a synthetic wrapper component that nests Page inside Layout(s)
113
+ * inside Wrapper(s).
114
+ *
115
+ * Nesting order: Wrapper(s) > Layout(s) > Page
116
+ *
117
+ * The wrapper's view reads `currentPage` from the mutable closure, so
118
+ * on client-side navigation only the Page sub-component changes while
119
+ * the Layout and Wrapper stay mounted with their state preserved.
120
+ */
121
+ function createLayoutWrapper(wrappers, layouts, Page) {
122
+ currentPage = Page;
123
+ currentPageName = Page.componentName || Page.name || 'VikePageComponent';
124
+ // Combined shell = wrappers (outermost) + layouts (innermost around Page)
125
+ // State keys: wrapper_0, wrapper_1, ..., layout_0, layout_1, ...
126
+ // Page state lives under the innermost shell component's slice.
127
+ const shell = [
128
+ ...wrappers.map((w, i) => ({ comp: w, key: 'wrapper_' + i })),
129
+ ...layouts.map((l, i) => ({ comp: l, key: 'layout_' + i })),
130
+ ];
131
+ function LayoutWrapperView({ state }) {
132
+ if (shell.length === 0) {
133
+ // No wrappers or layouts — render Page directly
134
+ return pageChildVNode(state.page);
135
+ }
136
+ const lastIdx = shell.length - 1;
137
+ const innermostState = state[shell[lastIdx].key] || {};
138
+ let inner = componentVNode(shell[lastIdx].comp, shell[lastIdx].key, [pageChildVNode(innermostState.page)], innermostState);
139
+ // Wrap with outer shell components
140
+ for (let i = lastIdx - 1; i >= 0; i--) {
141
+ inner = componentVNode(shell[i].comp, shell[i].key, [inner], state[shell[i].key]);
142
+ }
143
+ // Wrap in a plain div so the view returns a regular DOM vnode.
144
+ // Sub-component vnodes at the root position are not processed correctly
145
+ // by the rendering pipeline — they must be children of a DOM element.
146
+ return {
147
+ sel: 'div',
148
+ data: { attrs: { id: 'vike-shell' } },
149
+ children: [inner],
150
+ text: undefined,
151
+ elm: undefined,
152
+ key: '__vike_shell__',
153
+ };
154
+ }
155
+ LayoutWrapperView.componentName = 'VikeLayoutWrapper';
156
+ // Build the wrapper's initial state with a slice for each shell component.
157
+ // Page state is nested under the innermost shell component's slice.
158
+ const wrapperInitialState = {};
159
+ shell.forEach(({ comp, key }, i) => {
160
+ const sliceState = { ...(comp.initialState || {}) };
161
+ if (i === shell.length - 1) {
162
+ sliceState.page = Page.initialState || {};
163
+ }
164
+ wrapperInitialState[key] = sliceState;
165
+ });
166
+ LayoutWrapperView.initialState = wrapperInitialState;
167
+ // Context uses mutable references so navigation updates are picked up.
168
+ // Wrapper and Layout contexts are merged once; page-level context
169
+ // (pageData, routeParams, urlPathname) reads from mutable variables
170
+ // updated on each navigation.
171
+ const shellContext = {};
172
+ shell.forEach(({ comp }) => {
173
+ if (comp.context) {
174
+ Object.assign(shellContext, comp.context);
175
+ }
176
+ });
177
+ LayoutWrapperView.context = {
178
+ ...shellContext,
179
+ ...(Page.context || {}),
180
+ pageData: () => currentPageData,
181
+ routeParams: () => currentRouteParams,
182
+ urlPathname: () => currentUrlPathname,
183
+ };
184
+ return LayoutWrapperView;
185
+ }
186
+ /**
187
+ * Normalize a cumulative config value into an array of functions.
188
+ */
189
+ function toComponentArray(val) {
190
+ if (!val)
191
+ return [];
192
+ return (Array.isArray(val) ? val : [val]).filter((c) => typeof c === 'function');
193
+ }
21
194
  function onRenderClient(pageContext) {
22
195
  const { Page, config } = pageContext;
23
196
  const data = pageContext.data || {};
24
- // Dispose previous app on client-side navigation
25
- if (currentApp) {
26
- currentApp.dispose();
27
- currentApp = null;
28
- }
29
- // On first load with SSR, pick up server-serialized state.
30
- // On client navigation (or SPA mode), merge data into initialState.
31
- let initialState;
32
- if (typeof window !== 'undefined' && window.__VIKE_SYGNAL_STATE__ !== undefined) {
33
- initialState = window.__VIKE_SYGNAL_STATE__;
34
- // Clear the global after reading to avoid stale state on soft navigation
35
- delete window.__VIKE_SYGNAL_STATE__;
197
+ const wrappers = toComponentArray(config.Wrapper);
198
+ const layouts = toComponentArray(config.Layout);
199
+ const hasShell = wrappers.length > 0 || layouts.length > 0;
200
+ // The shell is wrappers (outermost) + layouts. The innermost component
201
+ // holds the Page state as a nested sub-component.
202
+ const shell = [
203
+ ...wrappers.map((_, i) => 'wrapper_' + i),
204
+ ...layouts.map((_, i) => 'layout_' + i),
205
+ ];
206
+ const innermostKey = shell.length > 0 ? shell[shell.length - 1] : null;
207
+ // Update mutable context references (used by the wrapper's context functions)
208
+ currentPageData = data;
209
+ currentRouteParams = pageContext.routeParams || {};
210
+ currentUrlPathname = pageContext.urlPathname || '';
211
+ // --- Shell path: hot-swap Page, keep Layout/Wrapper alive ---
212
+ if (hasShell) {
213
+ if (currentApp) {
214
+ // Client-side navigation: swap the Page without disposing the shell.
215
+ currentPage = Page;
216
+ currentPageName = Page.componentName || Page.name || 'VikePageComponent';
217
+ pageNavCounter++;
218
+ const newPageState = { ...(Page.initialState || {}), ...data };
219
+ if (currentApp.sinks?.STATE?.shamefullySendNext) {
220
+ currentApp.sinks.STATE.shamefullySendNext((state) => {
221
+ const shellState = state[innermostKey] || {};
222
+ return {
223
+ ...state,
224
+ [innermostKey]: { ...shellState, page: newPageState },
225
+ };
226
+ });
227
+ }
228
+ }
229
+ else {
230
+ // First load: read hydrated state or build from initialState
231
+ let initialState;
232
+ if (typeof window !== 'undefined' && window.__VIKE_SYGNAL_STATE__ !== undefined) {
233
+ initialState = window.__VIKE_SYGNAL_STATE__;
234
+ delete window.__VIKE_SYGNAL_STATE__;
235
+ }
236
+ else {
237
+ initialState = null;
238
+ }
239
+ if (initialState && innermostKey && initialState[innermostKey] !== undefined) {
240
+ // Hydrated combined state — distribute slices to each shell component
241
+ const allShellComps = [...wrappers, ...layouts];
242
+ shell.forEach((key, i) => {
243
+ if (initialState[key] !== undefined) {
244
+ if (i === shell.length - 1) {
245
+ const { page: pageState, ...compState } = initialState[key];
246
+ allShellComps[i].initialState = compState;
247
+ Page.initialState = pageState || { ...(Page.initialState || {}), ...data };
248
+ }
249
+ else {
250
+ allShellComps[i].initialState = initialState[key];
251
+ }
252
+ }
253
+ });
254
+ }
255
+ else {
256
+ // SPA mode or client-side first navigation
257
+ Page.initialState = { ...(Page.initialState || {}), ...data };
258
+ }
259
+ // Inject page-level context into Page (for SSR renderToString compat)
260
+ Page.context = {
261
+ ...Page.context,
262
+ pageData: () => currentPageData,
263
+ routeParams: () => currentRouteParams,
264
+ urlPathname: () => currentUrlPathname,
265
+ };
266
+ const Component = createLayoutWrapper(wrappers, layouts, Page);
267
+ try {
268
+ currentApp = sygnal.run(Component, {}, { mountPoint: '#page-view' });
269
+ }
270
+ catch (err) {
271
+ console.error('[sygnal/vike] Client render error:', err);
272
+ const container = document.getElementById('page-view');
273
+ if (container) {
274
+ container.innerHTML = `<div data-sygnal-error style="padding:2rem;color:#e74c3c;font-family:monospace">
275
+ <h2>Render Error</h2>
276
+ <pre>${String(err.message || err)}</pre>
277
+ </div>`;
278
+ }
279
+ }
280
+ }
281
+ // --- No shell path: dispose and recreate ---
36
282
  }
37
283
  else {
38
- initialState = {
39
- ...(Page.initialState || {}),
40
- ...data,
284
+ if (currentApp) {
285
+ currentApp.dispose();
286
+ currentApp = null;
287
+ }
288
+ let initialState;
289
+ if (typeof window !== 'undefined' && window.__VIKE_SYGNAL_STATE__ !== undefined) {
290
+ initialState = window.__VIKE_SYGNAL_STATE__;
291
+ delete window.__VIKE_SYGNAL_STATE__;
292
+ }
293
+ else {
294
+ initialState = { ...(Page.initialState || {}), ...data };
295
+ }
296
+ Page.initialState = initialState;
297
+ Page.context = {
298
+ ...Page.context,
299
+ pageData: () => currentPageData,
300
+ routeParams: () => currentRouteParams,
301
+ urlPathname: () => currentUrlPathname,
41
302
  };
42
- }
43
- // Set the initial state on the component before running
44
- Page.initialState = initialState;
45
- // Boot the Sygnal app into #page-view
46
- // Layout HTML lives outside #page-view, so it won't be destroyed
47
- try {
48
- currentApp = sygnal.run(Page, {}, { mountPoint: '#page-view' });
49
- }
50
- catch (err) {
51
- console.error('[sygnal/vike] Client render error:', err);
52
- const container = document.getElementById('page-view');
53
- if (container) {
54
- container.innerHTML = `<div data-sygnal-error style="padding:2rem;color:#e74c3c;font-family:monospace">
55
- <h2>Render Error</h2>
56
- <pre>${String(err.message || err)}</pre>
57
- </div>`;
303
+ try {
304
+ currentApp = sygnal.run(Page, {}, { mountPoint: '#page-view' });
305
+ }
306
+ catch (err) {
307
+ console.error('[sygnal/vike] Client render error:', err);
308
+ const container = document.getElementById('page-view');
309
+ if (container) {
310
+ container.innerHTML = `<div data-sygnal-error style="padding:2rem;color:#e74c3c;font-family:monospace">
311
+ <h2>Render Error</h2>
312
+ <pre>${String(err.message || err)}</pre>
313
+ </div>`;
314
+ }
58
315
  }
59
316
  }
60
317
  // Update document title on client navigation