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.
@@ -4,11 +4,15 @@
4
4
  * On first page load (hydration): reads server-serialized state and
5
5
  * boots the Sygnal app via run().
6
6
  *
7
- * On client-side navigation: disposes the previous app and boots
8
- * a fresh one for the new page.
7
+ * On client-side navigation with a Layout/Wrapper: the Layout and Wrapper
8
+ * stay mounted and only the Page sub-component is hot-swapped. Layout and
9
+ * Wrapper state persists across navigations. Without either, the app is
10
+ * disposed and recreated.
9
11
  *
10
- * Layout is rendered outside #page-view by onRenderHtml, so it persists
11
- * across navigations without being destroyed by Sygnal's DOM driver.
12
+ * Nesting order: Wrapper > Layout > Page
13
+ * - Wrappers are outermost (context providers, state management)
14
+ * - Layouts are inside wrappers (visual structure, navigation chrome)
15
+ * - Page is innermost (route-specific content, swapped on navigation)
12
16
  */
13
17
 
14
18
  // Import from the package entry so rollup externalizes it
@@ -24,56 +28,335 @@ declare global {
24
28
  interface PageContext {
25
29
  Page: any
26
30
  data?: any
31
+ routeParams?: Record<string, string>
32
+ urlPathname?: string
27
33
  isHydration?: boolean
28
34
  config: {
29
35
  Layout?: any | any[]
36
+ Wrapper?: any | any[]
30
37
  title?: string
31
38
  ssr?: boolean
32
39
  }
33
40
  }
34
41
 
35
42
  /** Track the running Sygnal app for disposal on navigation */
36
- let currentApp: { dispose: () => void } | null = null
43
+ let currentApp: { sources: any; sinks: any; dispose: () => void } | null = null
44
+
45
+ /**
46
+ * Mutable references read by the wrapper's view function.
47
+ * Updated on navigation to swap the Page without disposing the Layout.
48
+ */
49
+ let currentPage: any = null
50
+ let currentPageName: string = ''
51
+ let currentPageData: any = {}
52
+ let currentRouteParams: any = {}
53
+ let currentUrlPathname: string = ''
54
+ let pageNavCounter: number = 0
55
+
56
+ /**
57
+ * Build a component vnode that matches what the JSX pragma produces.
58
+ */
59
+ function componentVNode(comp: any, stateField: string, children: any[], compInitialState?: any): any {
60
+ const name = comp.componentName || comp.name || 'FUNCTION_COMPONENT'
61
+ return {
62
+ sel: name,
63
+ data: {
64
+ props: {
65
+ state: stateField,
66
+ sygnalOptions: {
67
+ name,
68
+ view: comp,
69
+ model: comp.model,
70
+ intent: comp.intent,
71
+ hmrActions: comp.hmrActions,
72
+ context: comp.context,
73
+ peers: comp.peers,
74
+ components: comp.components,
75
+ initialState: compInitialState,
76
+ isolatedState: true,
77
+ calculated: comp.calculated,
78
+ storeCalculatedInState: comp.storeCalculatedInState,
79
+ DOMSourceName: comp.DOMSourceName,
80
+ stateSourceName: comp.stateSourceName,
81
+ onError: comp.onError,
82
+ debug: comp.debug,
83
+ },
84
+ },
85
+ },
86
+ children,
87
+ text: undefined,
88
+ elm: undefined,
89
+ key: '__vike_' + stateField + '__',
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Build a vnode for the current Page as a child of the Layout.
95
+ * Reads from mutable `currentPage` / `currentPageName` so the wrapper
96
+ * view picks up the new Page component on navigation without being recreated.
97
+ */
98
+ function pageChildVNode(pageState: any): any {
99
+ // Include pageNavCounter in sel so instantiateSubComponents detects a
100
+ // component swap even when both pages have the same function name (e.g. 'Page').
101
+ const sel = currentPageName + '__nav' + pageNavCounter
102
+ return {
103
+ sel,
104
+ data: {
105
+ props: {
106
+ state: 'page',
107
+ sygnalOptions: {
108
+ name: sel,
109
+ view: currentPage,
110
+ model: currentPage.model,
111
+ intent: currentPage.intent,
112
+ hmrActions: currentPage.hmrActions,
113
+ context: currentPage.context,
114
+ peers: currentPage.peers,
115
+ components: currentPage.components,
116
+ initialState: pageState,
117
+ isolatedState: true,
118
+ calculated: currentPage.calculated,
119
+ storeCalculatedInState: currentPage.storeCalculatedInState,
120
+ DOMSourceName: currentPage.DOMSourceName,
121
+ stateSourceName: currentPage.stateSourceName,
122
+ onError: currentPage.onError,
123
+ debug: currentPage.debug,
124
+ },
125
+ },
126
+ },
127
+ children: [],
128
+ text: undefined,
129
+ elm: undefined,
130
+ key: '__vike_page__',
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Create a synthetic wrapper component that nests Page inside Layout(s)
136
+ * inside Wrapper(s).
137
+ *
138
+ * Nesting order: Wrapper(s) > Layout(s) > Page
139
+ *
140
+ * The wrapper's view reads `currentPage` from the mutable closure, so
141
+ * on client-side navigation only the Page sub-component changes while
142
+ * the Layout and Wrapper stay mounted with their state preserved.
143
+ */
144
+ function createLayoutWrapper(wrappers: any[], layouts: any[], Page: any): any {
145
+ currentPage = Page
146
+ currentPageName = Page.componentName || Page.name || 'VikePageComponent'
147
+
148
+ // Combined shell = wrappers (outermost) + layouts (innermost around Page)
149
+ // State keys: wrapper_0, wrapper_1, ..., layout_0, layout_1, ...
150
+ // Page state lives under the innermost shell component's slice.
151
+ const shell = [
152
+ ...wrappers.map((w: any, i: number) => ({ comp: w, key: 'wrapper_' + i })),
153
+ ...layouts.map((l: any, i: number) => ({ comp: l, key: 'layout_' + i })),
154
+ ]
155
+
156
+ function LayoutWrapperView({ state }: any) {
157
+ if (shell.length === 0) {
158
+ // No wrappers or layouts — render Page directly
159
+ return pageChildVNode(state.page)
160
+ }
161
+
162
+ const lastIdx = shell.length - 1
163
+ const innermostState = state[shell[lastIdx].key] || {}
164
+ let inner: any = componentVNode(
165
+ shell[lastIdx].comp,
166
+ shell[lastIdx].key,
167
+ [pageChildVNode(innermostState.page)],
168
+ innermostState
169
+ )
170
+
171
+ // Wrap with outer shell components
172
+ for (let i = lastIdx - 1; i >= 0; i--) {
173
+ inner = componentVNode(shell[i].comp, shell[i].key, [inner], state[shell[i].key])
174
+ }
175
+
176
+ // Wrap in a plain div so the view returns a regular DOM vnode.
177
+ // Sub-component vnodes at the root position are not processed correctly
178
+ // by the rendering pipeline — they must be children of a DOM element.
179
+ return {
180
+ sel: 'div',
181
+ data: { attrs: { id: 'vike-shell' } },
182
+ children: [inner],
183
+ text: undefined,
184
+ elm: undefined,
185
+ key: '__vike_shell__',
186
+ }
187
+ }
188
+
189
+ LayoutWrapperView.componentName = 'VikeLayoutWrapper'
190
+
191
+ // Build the wrapper's initial state with a slice for each shell component.
192
+ // Page state is nested under the innermost shell component's slice.
193
+ const wrapperInitialState: any = {}
194
+ shell.forEach(({ comp, key }: any, i: number) => {
195
+ const sliceState: any = { ...(comp.initialState || {}) }
196
+ if (i === shell.length - 1) {
197
+ sliceState.page = Page.initialState || {}
198
+ }
199
+ wrapperInitialState[key] = sliceState
200
+ })
201
+
202
+ LayoutWrapperView.initialState = wrapperInitialState
203
+
204
+ // Context uses mutable references so navigation updates are picked up.
205
+ // Wrapper and Layout contexts are merged once; page-level context
206
+ // (pageData, routeParams, urlPathname) reads from mutable variables
207
+ // updated on each navigation.
208
+ const shellContext: any = {}
209
+ shell.forEach(({ comp }: any) => {
210
+ if (comp.context) {
211
+ Object.assign(shellContext, comp.context)
212
+ }
213
+ })
214
+
215
+ LayoutWrapperView.context = {
216
+ ...shellContext,
217
+ ...(Page.context || {}),
218
+ pageData: () => currentPageData,
219
+ routeParams: () => currentRouteParams,
220
+ urlPathname: () => currentUrlPathname,
221
+ }
222
+
223
+ return LayoutWrapperView
224
+ }
225
+
226
+ /**
227
+ * Normalize a cumulative config value into an array of functions.
228
+ */
229
+ function toComponentArray(val: any): any[] {
230
+ if (!val) return []
231
+ return (Array.isArray(val) ? val : [val]).filter((c: any) => typeof c === 'function')
232
+ }
37
233
 
38
234
  export function onRenderClient(pageContext: PageContext) {
39
235
  const { Page, config } = pageContext
40
236
  const data = pageContext.data || {}
41
237
 
42
- // Dispose previous app on client-side navigation
43
- if (currentApp) {
44
- currentApp.dispose()
45
- currentApp = null
46
- }
238
+ const wrappers = toComponentArray(config.Wrapper)
239
+ const layouts = toComponentArray(config.Layout)
240
+ const hasShell = wrappers.length > 0 || layouts.length > 0
47
241
 
48
- // On first load with SSR, pick up server-serialized state.
49
- // On client navigation (or SPA mode), merge data into initialState.
50
- let initialState: any
51
- if (typeof window !== 'undefined' && window.__VIKE_SYGNAL_STATE__ !== undefined) {
52
- initialState = window.__VIKE_SYGNAL_STATE__
53
- // Clear the global after reading to avoid stale state on soft navigation
54
- delete window.__VIKE_SYGNAL_STATE__
242
+ // The shell is wrappers (outermost) + layouts. The innermost component
243
+ // holds the Page state as a nested sub-component.
244
+ const shell = [
245
+ ...wrappers.map((_: any, i: number) => 'wrapper_' + i),
246
+ ...layouts.map((_: any, i: number) => 'layout_' + i),
247
+ ]
248
+ const innermostKey = shell.length > 0 ? shell[shell.length - 1] : null
249
+
250
+ // Update mutable context references (used by the wrapper's context functions)
251
+ currentPageData = data
252
+ currentRouteParams = pageContext.routeParams || {}
253
+ currentUrlPathname = pageContext.urlPathname || ''
254
+
255
+ // --- Shell path: hot-swap Page, keep Layout/Wrapper alive ---
256
+ if (hasShell) {
257
+ if (currentApp) {
258
+ // Client-side navigation: swap the Page without disposing the shell.
259
+ currentPage = Page
260
+ currentPageName = Page.componentName || Page.name || 'VikePageComponent'
261
+ pageNavCounter++
262
+
263
+ const newPageState = { ...(Page.initialState || {}), ...data }
264
+ if (currentApp.sinks?.STATE?.shamefullySendNext) {
265
+ currentApp.sinks.STATE.shamefullySendNext((state: any) => {
266
+ const shellState = state[innermostKey!] || {}
267
+ return {
268
+ ...state,
269
+ [innermostKey!]: { ...shellState, page: newPageState },
270
+ }
271
+ })
272
+ }
273
+ } else {
274
+ // First load: read hydrated state or build from initialState
275
+ let initialState: any
276
+ if (typeof window !== 'undefined' && window.__VIKE_SYGNAL_STATE__ !== undefined) {
277
+ initialState = window.__VIKE_SYGNAL_STATE__
278
+ delete window.__VIKE_SYGNAL_STATE__
279
+ } else {
280
+ initialState = null
281
+ }
282
+
283
+ if (initialState && innermostKey && initialState[innermostKey] !== undefined) {
284
+ // Hydrated combined state — distribute slices to each shell component
285
+ const allShellComps = [...wrappers, ...layouts]
286
+ shell.forEach((key: string, i: number) => {
287
+ if (initialState[key] !== undefined) {
288
+ if (i === shell.length - 1) {
289
+ const { page: pageState, ...compState } = initialState[key]
290
+ allShellComps[i].initialState = compState
291
+ Page.initialState = pageState || { ...(Page.initialState || {}), ...data }
292
+ } else {
293
+ allShellComps[i].initialState = initialState[key]
294
+ }
295
+ }
296
+ })
297
+ } else {
298
+ // SPA mode or client-side first navigation
299
+ Page.initialState = { ...(Page.initialState || {}), ...data }
300
+ }
301
+
302
+ // Inject page-level context into Page (for SSR renderToString compat)
303
+ Page.context = {
304
+ ...Page.context,
305
+ pageData: () => currentPageData,
306
+ routeParams: () => currentRouteParams,
307
+ urlPathname: () => currentUrlPathname,
308
+ }
309
+
310
+ const Component = createLayoutWrapper(wrappers, layouts, Page)
311
+
312
+ try {
313
+ currentApp = run(Component, {}, { mountPoint: '#page-view' }) as any
314
+ } catch (err: any) {
315
+ console.error('[sygnal/vike] Client render error:', err)
316
+ const container = document.getElementById('page-view')
317
+ if (container) {
318
+ container.innerHTML = `<div data-sygnal-error style="padding:2rem;color:#e74c3c;font-family:monospace">
319
+ <h2>Render Error</h2>
320
+ <pre>${String(err.message || err)}</pre>
321
+ </div>`
322
+ }
323
+ }
324
+ }
325
+
326
+ // --- No shell path: dispose and recreate ---
55
327
  } else {
56
- initialState = {
57
- ...(Page.initialState || {}),
58
- ...data,
328
+ if (currentApp) {
329
+ currentApp.dispose()
330
+ currentApp = null
331
+ }
332
+
333
+ let initialState: any
334
+ if (typeof window !== 'undefined' && window.__VIKE_SYGNAL_STATE__ !== undefined) {
335
+ initialState = window.__VIKE_SYGNAL_STATE__
336
+ delete window.__VIKE_SYGNAL_STATE__
337
+ } else {
338
+ initialState = { ...(Page.initialState || {}), ...data }
339
+ }
340
+
341
+ Page.initialState = initialState
342
+ Page.context = {
343
+ ...Page.context,
344
+ pageData: () => currentPageData,
345
+ routeParams: () => currentRouteParams,
346
+ urlPathname: () => currentUrlPathname,
59
347
  }
60
- }
61
348
 
62
- // Set the initial state on the component before running
63
- Page.initialState = initialState
64
-
65
- // Boot the Sygnal app into #page-view
66
- // Layout HTML lives outside #page-view, so it won't be destroyed
67
- try {
68
- currentApp = run(Page, {}, { mountPoint: '#page-view' })
69
- } catch (err: any) {
70
- console.error('[sygnal/vike] Client render error:', err)
71
- const container = document.getElementById('page-view')
72
- if (container) {
73
- container.innerHTML = `<div data-sygnal-error style="padding:2rem;color:#e74c3c;font-family:monospace">
74
- <h2>Render Error</h2>
75
- <pre>${String(err.message || err)}</pre>
76
- </div>`
349
+ try {
350
+ currentApp = run(Page, {}, { mountPoint: '#page-view' }) as any
351
+ } catch (err: any) {
352
+ console.error('[sygnal/vike] Client render error:', err)
353
+ const container = document.getElementById('page-view')
354
+ if (container) {
355
+ container.innerHTML = `<div data-sygnal-error style="padding:2rem;color:#e74c3c;font-family:monospace">
356
+ <h2>Render Error</h2>
357
+ <pre>${String(err.message || err)}</pre>
358
+ </div>`
359
+ }
77
360
  }
78
361
  }
79
362
 
@@ -5,6 +5,10 @@
5
5
  * wraps it in a full HTML document, and embeds serialized state for
6
6
  * client hydration.
7
7
  *
8
+ * When a Layout is configured, the Layout HTML wraps the Page HTML
9
+ * inside #page-view so the structure matches the client-side wrapper
10
+ * component (see onRenderClient.ts).
11
+ *
8
12
  * Returns a plain HTML string. Vike wraps it with dangerouslySkipEscape
9
13
  * automatically. We avoid importing from vike/server because our bundled
10
14
  * output lives in node_modules/sygnal/ where vike isn't resolvable.
@@ -16,8 +20,11 @@ import { renderToString } from 'sygnal'
16
20
  interface PageContext {
17
21
  Page: any
18
22
  data?: any
23
+ routeParams?: Record<string, string>
24
+ urlPathname?: string
19
25
  config: {
20
26
  Layout?: any | any[]
27
+ Wrapper?: any | any[]
21
28
  Head?: any
22
29
  title?: string
23
30
  description?: string
@@ -69,12 +76,34 @@ export function onRenderHtml(pageContext: PageContext) {
69
76
  ...data,
70
77
  }
71
78
 
72
- // Render the page component to HTML, with error boundary
79
+ // Inject page data, route params, and URL into the component's context
80
+ // so sub-components can access them without prop drilling during SSR.
81
+ Page.context = {
82
+ ...Page.context,
83
+ pageData: () => data,
84
+ routeParams: () => pageContext.routeParams || {},
85
+ urlPathname: () => pageContext.urlPathname || '',
86
+ }
87
+
88
+ // Determine if Wrapper(s) and/or Layout(s) are present — this affects hydration state shape
89
+ const wrapperArray = config.Wrapper
90
+ ? (Array.isArray(config.Wrapper) ? config.Wrapper : [config.Wrapper])
91
+ .filter((W: any) => typeof W === 'function')
92
+ : []
93
+ const layoutArray = config.Layout
94
+ ? (Array.isArray(config.Layout) ? config.Layout : [config.Layout])
95
+ .filter((L: any) => typeof L === 'function')
96
+ : []
97
+ const hasShell = wrapperArray.length > 0 || layoutArray.length > 0
98
+
99
+ // Render the page component to HTML, with error boundary.
100
+ // When layouts are present, the hydration state is serialized separately
101
+ // as the wrapper's combined state (with page + layout slices).
73
102
  let pageHtml: string
74
103
  try {
75
104
  pageHtml = renderToString(Page, {
76
105
  state: initialState,
77
- hydrateState: '__VIKE_SYGNAL_STATE__',
106
+ hydrateState: hasShell ? false : '__VIKE_SYGNAL_STATE__',
78
107
  })
79
108
  } catch (err: any) {
80
109
  // If the component has an onError boundary, try rendering its fallback
@@ -83,8 +112,9 @@ export function onRenderHtml(pageContext: PageContext) {
83
112
  const fallbackVNode = Page.onError(err, { componentName: Page.name || 'Page' })
84
113
  if (fallbackVNode) {
85
114
  pageHtml = renderToString(() => fallbackVNode, { state: {} })
86
- // Still embed the state so the client can attempt hydration
87
- pageHtml += `<script>window.__VIKE_SYGNAL_STATE__=${JSON.stringify(initialState)}</script>`
115
+ if (!hasShell) {
116
+ pageHtml += `<script>window.__VIKE_SYGNAL_STATE__=${JSON.stringify(initialState)}</script>`
117
+ }
88
118
  } else {
89
119
  pageHtml = `<div data-sygnal-error>Render error</div>`
90
120
  }
@@ -97,34 +127,50 @@ export function onRenderHtml(pageContext: PageContext) {
97
127
  }
98
128
  }
99
129
 
100
- // If Layout(s) are defined, render them as static HTML wrapping the page.
101
- // The Layout is rendered OUTSIDE #page-view so that Sygnal's DOM driver
102
- // (which replaces the mount point content) doesn't destroy the Layout.
103
- let layoutBeforeHtml = ''
104
- let layoutAfterHtml = ''
105
- const layouts = config.Layout
106
- if (layouts) {
107
- const layoutArray = Array.isArray(layouts) ? layouts : [layouts]
108
- for (const Layout of layoutArray) {
109
- if (typeof Layout === 'function') {
110
- // Render the Layout with a placeholder for page content.
111
- // The Layout view receives { innerHTML: '<!--PAGE-->' } and we split
112
- // the output around the placeholder to get before/after chunks.
113
- const PLACEHOLDER = '<!--SYGNAL_PAGE_SLOT-->'
114
- const layoutHtml = renderToString(Layout, {
115
- state: Layout.initialState || {},
116
- props: { innerHTML: PLACEHOLDER },
117
- })
118
- const splitIdx = layoutHtml.indexOf(PLACEHOLDER)
119
- if (splitIdx !== -1) {
120
- layoutBeforeHtml += layoutHtml.substring(0, splitIdx)
121
- layoutAfterHtml = layoutHtml.substring(splitIdx + PLACEHOLDER.length) + layoutAfterHtml
122
- } else {
123
- // Fallback: Layout didn't use innerHTML, render it before page content
124
- layoutBeforeHtml += layoutHtml
125
- }
130
+ // If Layout(s) and/or Wrapper(s) are defined, render them wrapping the page content.
131
+ // HTML is rendered INSIDE #page-view so the structure matches the client-side
132
+ // wrapper component. Nesting order: Wrapper > Layout > Page.
133
+ // Each shell component receives page content via a placeholder in its children slot.
134
+ let pageViewContent = pageHtml
135
+ if (hasShell) {
136
+ // Combined shell: wrappers (outermost) + layouts (innermost around Page)
137
+ // Must match the order used in onRenderClient's createLayoutWrapper.
138
+ const shell = [
139
+ ...wrapperArray.map((w: any, i: number) => ({ comp: w, key: 'wrapper_' + i })),
140
+ ...layoutArray.map((l: any, i: number) => ({ comp: l, key: 'layout_' + i })),
141
+ ]
142
+
143
+ // Wrap from innermost to outermost (reverse order)
144
+ for (let i = shell.length - 1; i >= 0; i--) {
145
+ const { comp } = shell[i]
146
+ const PLACEHOLDER = '<!--SYGNAL_PAGE_SLOT-->'
147
+ const compHtml = renderToString(comp, {
148
+ state: comp.initialState || {},
149
+ props: { innerHTML: PLACEHOLDER },
150
+ })
151
+ const splitIdx = compHtml.indexOf(PLACEHOLDER)
152
+ if (splitIdx !== -1) {
153
+ pageViewContent = compHtml.substring(0, splitIdx) + pageViewContent + compHtml.substring(splitIdx + PLACEHOLDER.length)
154
+ } else {
155
+ // Fallback: component didn't use children/innerHTML, wrap content after it
156
+ pageViewContent = compHtml + pageViewContent
126
157
  }
127
158
  }
159
+ // Wrap in a shell div matching the client-side wrapper's root element
160
+ pageViewContent = `<div id="vike-shell">${pageViewContent}</div>`
161
+
162
+ // Serialize the wrapper's combined state for hydration.
163
+ // Must match the state shape from createLayoutWrapper:
164
+ // { wrapper_0: {...}, layout_0: { ...layoutState, page: pageState } }
165
+ const wrapperState: any = {}
166
+ shell.forEach(({ comp, key }: any, i: number) => {
167
+ const compState: any = { ...(comp.initialState || {}) }
168
+ if (i === shell.length - 1) {
169
+ compState.page = initialState
170
+ }
171
+ wrapperState[key] = compState
172
+ })
173
+ pageViewContent += `<script>window.__VIKE_SYGNAL_STATE__=${JSON.stringify(wrapperState)}</script>`
128
174
  }
129
175
 
130
176
  // Render Head component for <head> tags
@@ -161,9 +207,7 @@ export function onRenderHtml(pageContext: PageContext) {
161
207
  ${headContent}
162
208
  </head>
163
209
  <body>
164
- ${layoutBeforeHtml}
165
- <div id="page-view">${pageHtml}</div>
166
- ${layoutAfterHtml}
210
+ <div id="page-view">${pageViewContent}</div>
167
211
  </body>
168
212
  </html>`
169
213
 
package/src/vike/types.ts CHANGED
@@ -10,6 +10,8 @@ declare global {
10
10
  interface Config {
11
11
  /** Sygnal component to wrap all pages (receives children) */
12
12
  Layout?: any
13
+ /** Sygnal component wrapping Layout + Page (for context providers, state management). Cumulative. */
14
+ Wrapper?: any
13
15
  /** Sygnal component rendered inside <head> for per-page meta tags */
14
16
  Head?: any
15
17
  /** Page <title> */
@@ -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:
@@ -52,9 +52,11 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
52
52
  if (disableJsx) return
53
53
 
54
54
  return {
55
- esbuild: {
56
- jsx: 'automatic' as const,
57
- jsxImportSource: 'sygnal',
55
+ oxc: {
56
+ jsx: {
57
+ runtime: 'automatic' as const,
58
+ importSource: 'sygnal',
59
+ },
58
60
  },
59
61
  }
60
62
  },