sygnal 5.4.0 → 6.0.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.
Files changed (316) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/README.md +24 -9
  3. package/dist/astro/client.cjs.js +16 -9
  4. package/dist/astro/client.cjs.js.map +1 -1
  5. package/dist/astro/client.mjs +16 -9
  6. package/dist/astro/client.mjs.map +1 -1
  7. package/dist/astro/index.cjs.js +362 -91
  8. package/dist/astro/index.cjs.js.map +1 -1
  9. package/dist/astro/index.mjs +362 -92
  10. package/dist/astro/index.mjs.map +1 -1
  11. package/dist/astro/server.cjs.js +3396 -165
  12. package/dist/astro/server.cjs.js.map +1 -1
  13. package/dist/astro/server.mjs +3396 -165
  14. package/dist/astro/server.mjs.map +1 -1
  15. package/dist/devtools.cjs.js +1431 -0
  16. package/dist/devtools.cjs.js.map +1 -0
  17. package/dist/devtools.esm.js +1419 -0
  18. package/dist/devtools.esm.js.map +1 -0
  19. package/dist/diagnostics.cjs.js +3032 -296
  20. package/dist/diagnostics.cjs.js.map +1 -1
  21. package/dist/diagnostics.esm.js +3032 -296
  22. package/dist/diagnostics.esm.js.map +1 -1
  23. package/dist/element.cjs.js +230 -0
  24. package/dist/element.cjs.js.map +1 -0
  25. package/dist/element.esm.js +228 -0
  26. package/dist/element.esm.js.map +1 -0
  27. package/dist/guide/accessibility.md +257 -0
  28. package/dist/guide/adapters.md +221 -0
  29. package/dist/guide/behaviors.md +312 -0
  30. package/dist/guide/browser-sources.md +203 -0
  31. package/dist/guide/drag-and-drop.md +342 -0
  32. package/dist/guide/element-commands.md +306 -0
  33. package/dist/guide/error-boundaries.md +169 -0
  34. package/dist/guide/forms-reference.md +381 -0
  35. package/dist/guide/forms.md +250 -0
  36. package/dist/guide/http.md +299 -0
  37. package/dist/guide/inputs.md +173 -0
  38. package/dist/guide/persistence.md +245 -0
  39. package/dist/guide/recipes/carousel.md +155 -0
  40. package/dist/guide/recipes/charts.md +177 -0
  41. package/dist/guide/recipes/code-editor.md +135 -0
  42. package/dist/guide/recipes/data-grid.md +155 -0
  43. package/dist/guide/recipes/data-table.md +196 -0
  44. package/dist/guide/recipes/i18n.md +222 -0
  45. package/dist/guide/recipes/icons.md +115 -0
  46. package/dist/guide/recipes/overview.md +33 -0
  47. package/dist/guide/recipes/rich-text.md +154 -0
  48. package/dist/guide/resources.md +471 -0
  49. package/dist/guide/ssr.md +255 -0
  50. package/dist/guide/timers.md +178 -0
  51. package/dist/guide/ui/accordion.md +93 -0
  52. package/dist/guide/ui/combobox.md +144 -0
  53. package/dist/guide/ui/dialog.md +124 -0
  54. package/dist/guide/ui/disclosure.md +60 -0
  55. package/dist/guide/ui/menu.md +125 -0
  56. package/dist/guide/ui/overview.md +166 -0
  57. package/dist/guide/ui/popover.md +101 -0
  58. package/dist/guide/ui/select.md +114 -0
  59. package/dist/guide/ui/tabs.md +161 -0
  60. package/dist/guide/ui/toaster.md +152 -0
  61. package/dist/guide/ui/tooltip.md +103 -0
  62. package/dist/guide/undo.md +178 -0
  63. package/dist/guide/virtual-collections.md +183 -0
  64. package/dist/guide/web-components.md +179 -0
  65. package/dist/guide/widgets.md +217 -0
  66. package/dist/index.cjs.js +13178 -5769
  67. package/dist/index.cjs.js.map +1 -1
  68. package/dist/index.d.ts +2908 -276
  69. package/dist/index.esm.js +13149 -5779
  70. package/dist/index.esm.js.map +1 -1
  71. package/dist/jsx-dev-runtime.cjs.js +26 -334
  72. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  73. package/dist/jsx-dev-runtime.esm.js +19 -327
  74. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  75. package/dist/jsx-runtime.cjs.js +26 -334
  76. package/dist/jsx-runtime.cjs.js.map +1 -1
  77. package/dist/jsx-runtime.esm.js +19 -327
  78. package/dist/jsx-runtime.esm.js.map +1 -1
  79. package/dist/jsx.cjs.js +10 -314
  80. package/dist/jsx.cjs.js.map +1 -1
  81. package/dist/jsx.esm.js +8 -315
  82. package/dist/jsx.esm.js.map +1 -1
  83. package/dist/react.cjs.js +117 -0
  84. package/dist/react.cjs.js.map +1 -0
  85. package/dist/react.esm.js +115 -0
  86. package/dist/react.esm.js.map +1 -0
  87. package/dist/shims/globalthis.cjs +20 -0
  88. package/dist/sygnal.min.js +1 -1
  89. package/dist/sygnal.min.js.map +1 -1
  90. package/dist/ui-combobox.cjs.js +169 -0
  91. package/dist/ui-combobox.cjs.js.map +1 -0
  92. package/dist/ui-combobox.esm.js +148 -0
  93. package/dist/ui-combobox.esm.js.map +1 -0
  94. package/dist/ui-menu.cjs.js +87 -0
  95. package/dist/ui-menu.cjs.js.map +1 -0
  96. package/dist/ui-menu.esm.js +66 -0
  97. package/dist/ui-menu.esm.js.map +1 -0
  98. package/dist/ui-select.cjs.js +104 -0
  99. package/dist/ui-select.cjs.js.map +1 -0
  100. package/dist/ui-select.esm.js +83 -0
  101. package/dist/ui-select.esm.js.map +1 -0
  102. package/dist/ui.cjs.js +738 -0
  103. package/dist/ui.cjs.js.map +1 -0
  104. package/dist/ui.esm.js +727 -0
  105. package/dist/ui.esm.js.map +1 -0
  106. package/dist/vike/ClientOnly.cjs.js +4 -14
  107. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  108. package/dist/vike/ClientOnly.mjs +4 -14
  109. package/dist/vike/ClientOnly.mjs.map +1 -1
  110. package/dist/vike/{+config.js → config/+config.js} +9 -1
  111. package/dist/vike/config/+config.js.map +1 -0
  112. package/dist/vike/config/package.json +6 -0
  113. package/dist/vike/onRenderClient.cjs.js +165 -95
  114. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  115. package/dist/vike/onRenderClient.mjs +165 -95
  116. package/dist/vike/onRenderClient.mjs.map +1 -1
  117. package/dist/vike/onRenderHtml.cjs.js +59 -24
  118. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  119. package/dist/vike/onRenderHtml.mjs +60 -25
  120. package/dist/vike/onRenderHtml.mjs.map +1 -1
  121. package/dist/vite/plugin.cjs.js +323 -88
  122. package/dist/vite/plugin.cjs.js.map +1 -1
  123. package/dist/vite/plugin.mjs +323 -89
  124. package/dist/vite/plugin.mjs.map +1 -1
  125. package/dist/zag.cjs.js +408 -0
  126. package/dist/zag.cjs.js.map +1 -0
  127. package/dist/zag.esm.js +405 -0
  128. package/dist/zag.esm.js.map +1 -0
  129. package/llms.txt +166 -103
  130. package/package.json +98 -14
  131. package/src/astro/client.ts +27 -16
  132. package/src/astro/index.d.ts +24 -0
  133. package/src/astro/index.ts +69 -3
  134. package/src/astro/server.ts +8 -2
  135. package/src/collection.ts +6 -93
  136. package/src/core/actions.ts +134 -0
  137. package/src/core/cell.ts +202 -0
  138. package/src/core/debug.ts +23 -0
  139. package/src/core/define.ts +242 -0
  140. package/src/core/hooks.ts +314 -0
  141. package/src/core/hosts/collection.ts +325 -0
  142. package/src/core/hosts/switchable.ts +146 -0
  143. package/src/core/instance.ts +592 -0
  144. package/src/core/markers/clientonly.ts +13 -0
  145. package/src/core/markers/lazy.ts +40 -0
  146. package/src/core/markers/portal.ts +96 -0
  147. package/src/core/markers/suspense.ts +41 -0
  148. package/src/core/markers/transition.ts +70 -0
  149. package/src/core/registry.ts +25 -0
  150. package/src/core/runtime.ts +573 -0
  151. package/src/core/statics.ts +104 -0
  152. package/src/core/teardown.ts +60 -0
  153. package/src/core/view.ts +37 -0
  154. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  155. package/src/cycle/dom/ElementFinder.ts +7 -9
  156. package/src/cycle/dom/EventDelegator.ts +174 -223
  157. package/src/cycle/dom/IsolateModule.ts +65 -35
  158. package/src/cycle/dom/MainDOMSource.ts +9 -2
  159. package/src/cycle/dom/PriorityQueue.ts +4 -2
  160. package/src/cycle/dom/SymbolTree.ts +18 -47
  161. package/src/cycle/dom/classNameModule.ts +9 -7
  162. package/src/cycle/dom/controlledInputModule.ts +23 -6
  163. package/src/cycle/dom/enrichEventStream.ts +25 -47
  164. package/src/cycle/dom/fragment.ts +7 -0
  165. package/src/cycle/dom/isolate.ts +30 -27
  166. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  167. package/src/cycle/dom/mockDOMSource.ts +21 -1
  168. package/src/cycle/dom/modules.ts +16 -3
  169. package/src/cycle/dom/propsModule.ts +65 -0
  170. package/src/cycle/dom/selectModule.ts +22 -17
  171. package/src/cycle/dom/snabbdom.ts +1 -6
  172. package/src/cycle/dom/thunk.ts +3 -0
  173. package/src/cycle/dom/utils.ts +98 -0
  174. package/src/cycle/dom/viewTransition.ts +43 -0
  175. package/src/cycle/state/StateSource.ts +19 -3
  176. package/src/cycle/state/objIsEqual.ts +50 -0
  177. package/src/cycle/state/types.ts +0 -10
  178. package/src/defineComponent.ts +34 -0
  179. package/src/devtools.d.ts +151 -0
  180. package/src/devtools.ts +34 -0
  181. package/src/element.d.ts +59 -0
  182. package/src/element.ts +235 -0
  183. package/src/extra/backoff.ts +9 -0
  184. package/src/extra/behaviors.ts +169 -0
  185. package/src/extra/browserSignals.ts +23 -0
  186. package/src/extra/browserSources.ts +326 -0
  187. package/src/extra/command.ts +3 -3
  188. package/src/extra/controls.ts +47 -0
  189. package/src/extra/copyAsTest.ts +286 -0
  190. package/src/extra/devtools.ts +101 -31
  191. package/src/extra/devtoolsActions.ts +379 -0
  192. package/src/extra/devtoolsHook.ts +9 -0
  193. package/src/extra/devtoolsNext.ts +83 -0
  194. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  195. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  196. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  197. package/src/extra/diagnostics/checks/collections.ts +1 -1
  198. package/src/extra/diagnostics/checks/controls.ts +148 -0
  199. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  200. package/src/extra/diagnostics/checks/dom.ts +20 -11
  201. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  202. package/src/extra/diagnostics/checks/events.ts +17 -2
  203. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  204. package/src/extra/diagnostics/checks/forms.ts +163 -0
  205. package/src/extra/diagnostics/checks/index.ts +97 -4
  206. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  207. package/src/extra/diagnostics/checks/next.ts +417 -0
  208. package/src/extra/diagnostics/checks/persist.ts +54 -0
  209. package/src/extra/diagnostics/checks/props.ts +10 -5
  210. package/src/extra/diagnostics/checks/public.d.ts +98 -7
  211. package/src/extra/diagnostics/checks/replies.ts +128 -0
  212. package/src/extra/diagnostics/checks/router.ts +100 -0
  213. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  214. package/src/extra/diagnostics/checks/shared.ts +86 -2
  215. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  216. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  217. package/src/extra/diagnostics/checks/state.ts +100 -7
  218. package/src/extra/diagnostics/checks/statics.ts +48 -0
  219. package/src/extra/diagnostics/checks/strict.ts +10 -67
  220. package/src/extra/diagnostics/checks/timers.ts +80 -0
  221. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  222. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  223. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  224. package/src/extra/diagnostics/checks/wiring.ts +66 -10
  225. package/src/extra/diagnostics/codes.ts +268 -23
  226. package/src/extra/diagnostics/index.ts +34 -9
  227. package/src/extra/diagnostics/legacy.ts +17 -0
  228. package/src/extra/driverFactories.ts +83 -19
  229. package/src/extra/elementCommands.ts +53 -0
  230. package/src/extra/fetchDriver.ts +519 -0
  231. package/src/extra/focusWithin.ts +27 -0
  232. package/src/extra/form.ts +268 -0
  233. package/src/extra/formHelpers.ts +139 -0
  234. package/src/extra/head.ts +106 -0
  235. package/src/extra/hmr.ts +2 -3
  236. package/src/extra/owned.ts +24 -0
  237. package/src/extra/pager.ts +50 -0
  238. package/src/extra/persist.ts +133 -0
  239. package/src/extra/pwa.ts +1 -1
  240. package/src/extra/queryCache.ts +107 -0
  241. package/src/extra/reducers.ts +1 -1
  242. package/src/extra/reduxDevtools.ts +92 -0
  243. package/src/extra/replies.ts +54 -0
  244. package/src/extra/router.ts +323 -0
  245. package/src/extra/run.ts +90 -217
  246. package/src/extra/selection.ts +76 -0
  247. package/src/extra/socketDriver.ts +265 -0
  248. package/src/extra/sortable.ts +377 -0
  249. package/src/extra/ssr.ts +427 -160
  250. package/src/extra/standardSchema.ts +27 -0
  251. package/src/extra/testing.ts +2433 -175
  252. package/src/extra/timers.ts +95 -0
  253. package/src/extra/undo.ts +261 -0
  254. package/src/extra/viewTransitions.ts +38 -0
  255. package/src/extra/virtual.ts +513 -0
  256. package/src/extra/widget.ts +201 -0
  257. package/src/index.d.ts +2631 -117
  258. package/src/index.ts +25 -4
  259. package/src/jsx-runtime.ts +15 -1
  260. package/src/jsx.ts +2 -1
  261. package/src/lazy.ts +50 -7
  262. package/src/portal.ts +3 -1
  263. package/src/pragma/index.ts +256 -133
  264. package/src/react-peers.d.ts +5 -0
  265. package/src/react.d.ts +43 -0
  266. package/src/react.ts +110 -0
  267. package/src/shared.ts +48 -0
  268. package/src/slot.ts +1 -1
  269. package/src/suspense.ts +3 -1
  270. package/src/switchable.ts +6 -121
  271. package/src/transition.ts +3 -1
  272. package/src/ui/accordion.ts +64 -0
  273. package/src/ui/dialog.ts +129 -0
  274. package/src/ui/disclosure.ts +35 -0
  275. package/src/ui/popover.ts +45 -0
  276. package/src/ui/shared.ts +94 -0
  277. package/src/ui/tabs.ts +82 -0
  278. package/src/ui/toaster.ts +198 -0
  279. package/src/ui/tooltip.ts +70 -0
  280. package/src/ui/zag/combobox.ts +115 -0
  281. package/src/ui/zag/menu.ts +40 -0
  282. package/src/ui/zag/select.ts +54 -0
  283. package/src/ui/zag/shared.ts +43 -0
  284. package/src/ui-combobox.d.ts +20 -0
  285. package/src/ui-combobox.ts +6 -0
  286. package/src/ui-menu.d.ts +30 -0
  287. package/src/ui-menu.ts +6 -0
  288. package/src/ui-select.d.ts +17 -0
  289. package/src/ui-select.ts +6 -0
  290. package/src/ui-zag-types.d.ts +42 -0
  291. package/src/ui.d.ts +208 -0
  292. package/src/ui.ts +15 -0
  293. package/src/vike/+config.ts +9 -1
  294. package/src/vike/ClientOnly.ts +1 -1
  295. package/src/vike/onRenderClient.ts +144 -96
  296. package/src/vike/onRenderHtml.ts +69 -26
  297. package/src/vike/types.ts +7 -1
  298. package/src/vite/globalthis-shim.ts +18 -0
  299. package/src/vite/globalthis.ts +56 -0
  300. package/src/vite/plugin.d.ts +22 -3
  301. package/src/vite/plugin.ts +222 -24
  302. package/src/zag.d.ts +64 -0
  303. package/src/zag.ts +238 -0
  304. package/dist/vike/+config.cjs.js +0 -66
  305. package/dist/vike/+config.cjs.js.map +0 -1
  306. package/dist/vike/+config.js.map +0 -1
  307. package/src/component.ts +0 -2301
  308. package/src/cycle/isolate/index.ts +0 -196
  309. package/src/cycle/run/index.ts +0 -151
  310. package/src/cycle/run/internals.ts +0 -143
  311. package/src/cycle/state/Collection.ts +0 -184
  312. package/src/cycle/state/index.ts +0 -5
  313. package/src/cycle/state/pickCombine.ts +0 -201
  314. package/src/cycle/state/pickMerge.ts +0 -122
  315. package/src/cycle/state/withState.ts +0 -47
  316. package/src/pragma/fn.ts +0 -55
@@ -1,7 +1,59 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
- import { pathToFileURL } from 'node:url';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+
6
+ /**
7
+ * G-099: xstream does `require('globalthis').getPolyfill()`, which pulls the
8
+ * `globalthis` polyfill and its dependency chain (define-properties,
9
+ * get-intrinsic, object-keys, has-symbols, ...; ~4 KB gzip) into every app
10
+ * bundle. Every runtime Sygnal supports has a native `globalThis`, so
11
+ * `sygnal/vite` (and the sygnal/astro integration) alias the package to
12
+ * dist/shims/globalthis.cjs (src/vite/globalthis-shim.ts) unless
13
+ * `nativeGlobalThis: false`. A `resolve.alias`, not a resolveId hook: Vite's
14
+ * dependency pre-bundling only applies aliases.
15
+ */
16
+ // Node built-ins (this runs in Node). The package has no @types/node.
17
+ // @ts-ignore
18
+ /**
19
+ * Absolute path of the stub in the sygnal package this code belongs to (found
20
+ * by walking up from this file: dist/vite, dist/astro or src/vite), or
21
+ * undefined when it is missing (e.g. sygnal not built).
22
+ */
23
+ function globalThisShim() {
24
+ try {
25
+ let dir = path.dirname(fileURLToPath(import.meta.url));
26
+ while (dir !== path.dirname(dir)) {
27
+ const pkgFile = path.join(dir, 'package.json');
28
+ if (fs.existsSync(pkgFile) && JSON.parse(fs.readFileSync(pkgFile, 'utf8')).name === 'sygnal') {
29
+ const file = path.join(dir, 'dist', 'shims', 'globalthis.cjs');
30
+ return fs.existsSync(file) ? file : undefined;
31
+ }
32
+ dir = path.dirname(dir);
33
+ }
34
+ }
35
+ catch (_) { }
36
+ return undefined;
37
+ }
38
+ /**
39
+ * Whether a Vite `resolve.alias` value (object or array form) already maps
40
+ * `globalthis` (R2-7): the user's alias wins and the stub is not added.
41
+ */
42
+ function hasGlobalThisAlias(alias) {
43
+ if (!alias)
44
+ return false;
45
+ if (!Array.isArray(alias))
46
+ return Object.prototype.hasOwnProperty.call(alias, 'globalthis');
47
+ return alias.some((a) => a && (a.find instanceof RegExp ? a.find.test('globalthis') : a.find === 'globalthis'));
48
+ }
49
+ /**
50
+ * Vite `resolve.alias` entries for the stub (empty when it is missing, or
51
+ * when `userAlias` — the user's `resolve.alias` — already maps `globalthis`).
52
+ */
53
+ function globalThisAlias(userAlias) {
54
+ const shim = hasGlobalThisAlias(userAlias) ? undefined : globalThisShim();
55
+ return shim ? [{ find: /^globalthis$/, replacement: shim }] : [];
56
+ }
5
57
 
6
58
  /**
7
59
  * Sygnal Vite Plugin
@@ -17,7 +69,10 @@ import { pathToFileURL } from 'node:url';
17
69
  * What it does:
18
70
  * 1. Configures the automatic JSX transform with sygnal as the import source:
19
71
  * `oxc` (Vite 8), plus `esbuild` under Vite 7 and older
20
- * 2. Detects files that call `run()` from sygnal and auto-injects HMR wiring
72
+ * 2. Detects files that call `run()` from sygnal and auto-injects HMR wiring.
73
+ * A module that calls `defineElement()` from 'sygnal/element' at the top level
74
+ * (and doesn't import run()) gets `if (import.meta.hot) import.meta.hot.accept()`
75
+ * appended: re-running it swaps the elements' component, keeping their state
21
76
  * 3. Dev mode (`vite` / `vite dev` only; never `vite build`). Into every
22
77
  * file that imports `run` from sygnal (including entries with manual HMR
23
78
  * wiring or `disableHmr`) it inserts, on an existing line:
@@ -53,9 +108,14 @@ import { pathToFileURL } from 'node:url';
53
108
  * current findings when the page loads ('sygnal:check:request'), and only
54
109
  * that client is answered; after a source change the new findings go to
55
110
  * every connected client, and only when they changed. Error-severity
56
- * findings also open Vite's error overlay (`overlay: false`: never).
57
- * sygnal-check's codes are currently all warnings or info, so in practice
58
- * everything goes to the console. Warnings never use the overlay: while
111
+ * findings also open Vite's error overlay (`overlay: false`: never):
112
+ * SYG112, SYG124, SYG125, SYG128, SYG405 and the checker's other
113
+ * error-severity codes. The a11y lane SYG701-708 is a warning, also
114
+ * under `diagnostics.strict` (D144, amending D111); `check.a11y: 'error'`
115
+ * makes it an error (and opens the overlay). Without that opt-in a 7xx
116
+ * finding never opens the overlay, even from an older sygnal-check that
117
+ * reports it as an error under strict. Everything else is a warning or
118
+ * info and goes to the console. Warnings never use the overlay: while
59
119
  * one is open Vite's client reloads the page on the next HMR update
60
120
  * (`overlay: 'warn'` is treated as 'error', with a notice). The dev
61
121
  * client closes a sygnal-check overlay before each update, and the
@@ -76,13 +136,31 @@ import { pathToFileURL } from 'node:url';
76
136
  * 7. Vike and Astro (G-014): the app is started by sygnal's own client
77
137
  * entry, which user code doesn't import. Client-side imports of
78
138
  * 'sygnal/vike/onRenderClient' resolve to a dev wrapper that sets the
79
- * flag and loads the checks first. Astro resolves its renderer entry on
139
+ * flag and loads the checks first. That entry is excluded from
140
+ * pre-bundling in dev (B-020), so it shares the pages' core. Astro resolves its renderer entry on
80
141
  * the server and the browser loads the file itself, so the same snippet
81
142
  * is added to sygnal's 'astro/client' file by the transform. That file
82
143
  * imports the public 'sygnal' entry (one shared core, B-019), so its
83
144
  * 'sygnal' import gets the run() wrapper too: every `diagnostics` mode
84
145
  * and ignore list works for islands. The sygnal/astro integration adds
85
146
  * this plugin in `astro dev`.
147
+ * 8. `nativeGlobalThis` (G-099, default true): a `resolve.alias` from
148
+ * `globalthis` (xstream's `require('globalthis').getPolyfill()`) to the
149
+ * package's dist/shims/globalthis.cjs, which returns the native
150
+ * globalThis. Drops the polyfill chain (~4 KB gzip) from every bundle,
151
+ * in dev (including pre-bundling), build and Vitest. Dependencies left
152
+ * external (SSR, Vitest) still load the real package from Node. The
153
+ * sygnal/astro integration adds the same alias in `astro build`. Any
154
+ * dependency's `require('globalthis')` gets the stub; a `globalthis`
155
+ * entry in the user's own `resolve.alias` wins (R2-7).
156
+ * 9. `devtools` option (D77, default true): in dev the same files (run()
157
+ * importers, the Vike wrapper, the Astro client) also get
158
+ * `import 'sygnal/devtools';`, first, which installs the DevTools bridge
159
+ * (window.__SYGNAL_DEVTOOLS__) for the browser extension. Independent of
160
+ * `diagnostics` ('off' still injects it). Never in `vite build` or under
161
+ * Vitest: production builds carry no DevTools code. `devtools: false`
162
+ * injects nothing. PLAN-4 3-E: `devtools: { redux: true }` also calls
163
+ * connectReduxDevtools() (actions + state to the Redux DevTools extension).
86
164
  *
87
165
  * Why not Vite's `define`? Vite's dependency optimizer does not apply user
88
166
  * `define` replacements to pre-bundled dependencies (only process.env.NODE_ENV),
@@ -127,7 +205,7 @@ const OVERLAY_PLUGIN = 'sygnal-check';
127
205
  const REINSTALL_IMPORT = `import { installChecks as __sygnalInstallChecks } from 'sygnal/diagnostics';`;
128
206
  const REINSTALL_CALL = 'try { __sygnalInstallChecks() } catch (e) { console.warn(e) }\n';
129
207
  function sygnal(options = {}) {
130
- const { disableJsx = false, disableHmr = false, vitestSetup = true } = options;
208
+ const { disableJsx = false, disableHmr = false, vitestSetup = true, nativeGlobalThis = true } = options;
131
209
  const diagnostics = normalizeDiagnostics(options.diagnostics);
132
210
  const devOn = diagnostics.mode !== 'off';
133
211
  // A mode other than the flag's 'warn', or an ignore list, needs run()'s option
@@ -140,6 +218,15 @@ function sygnal(options = {}) {
140
218
  // Statements run before the app starts: dev flag (+ strict flag)
141
219
  const flags = DEV_FLAG + (diagnostics.strict ? STRICT_FLAG : '');
142
220
  const devImports = `import 'sygnal/diagnostics';import '${DEV_CLIENT}';`;
221
+ // D77: the DevTools bridge, dev only. First, so the diagnostics entry finds it.
222
+ const devtoolsOn = options.devtools !== false;
223
+ const redux = typeof options.devtools == 'object' && !!options.devtools?.redux;
224
+ const devtoolsImport = !devtoolsOn ? ''
225
+ : redux ? `import { connectReduxDevtools as __sygnalReduxDevtools } from 'sygnal/devtools';__sygnalReduxDevtools();`
226
+ : `import 'sygnal/devtools';`;
227
+ // What a dev entry gets: DevTools, then (unless diagnostics are 'off') flags + checks
228
+ const devSnippet = devtoolsImport + (devOn ? flags + devImports : '');
229
+ const devInject = devOn || devtoolsOn;
143
230
  let astroClient;
144
231
  return {
145
232
  name: 'vite-plugin-sygnal',
@@ -155,6 +242,12 @@ function sygnal(options = {}) {
155
242
  noExternal: ['sygnal'],
156
243
  },
157
244
  };
245
+ // G-099: xstream's `require('globalthis')` gets the native-globalThis
246
+ // stub. An alias (not resolveId) so it also applies to pre-bundling.
247
+ // R2-7: not when the user's config already aliases `globalthis`.
248
+ const alias = nativeGlobalThis ? globalThisAlias(config?.resolve?.alias) : [];
249
+ if (alias.length)
250
+ result.resolve = { alias };
158
251
  if (!disableJsx) {
159
252
  result.oxc = {
160
253
  jsx: {
@@ -186,13 +279,31 @@ function sygnal(options = {}) {
186
279
  result.test = { setupFiles: [setup] };
187
280
  }
188
281
  }
189
- else if (isServe && devOn && sygnalInNodeModules(root)) {
282
+ else if (isServe && devInject && sygnalInNodeModules(root)) {
190
283
  // The dependency scan reads the original sources, which don't import
191
- // the checks entry: pre-bundle it with sygnal so the first page load
192
- // doesn't trigger a re-optimization. Only for an installed sygnal: a
193
- // linked one is served from source, and pre-bundling the checks alone
194
- // would give them their own copy of the core.
195
- result.optimizeDeps = { ...result.optimizeDeps, include: ['sygnal/diagnostics'] };
284
+ // the checks (or DevTools) entry: pre-bundle it with sygnal so the first
285
+ // page load doesn't trigger a re-optimization. Only for an installed
286
+ // sygnal: a linked one is served from source, and pre-bundling the
287
+ // checks alone would give them their own copy of the core.
288
+ const include = [...(devtoolsOn ? ['sygnal/devtools'] : []), ...(devOn ? ['sygnal/diagnostics'] : [])];
289
+ result.optimizeDeps = { ...result.optimizeDeps, include };
290
+ }
291
+ // B-020: Vike adds its client entry 'sygnal/vike/onRenderClient' to
292
+ // optimizeDeps.include. With a linked sygnal, the pages' 'sygnal' is
293
+ // served from source while that pre-bundle inlines a second copy of the
294
+ // core. Unbundled, the entry's own `import 'sygnal'` resolves like the
295
+ // pages' imports (source when linked, the shared pre-bundle when
296
+ // installed): one core. Vike honours the exclude list.
297
+ // G-098: Vite doesn't discover the imports of an excluded dependency, so for an
298
+ // installed sygnal 'sygnal' is pre-bundled up front (pages that reach it only through
299
+ // the JSX runtime would otherwise get it, and its CommonJS xstream, unbundled).
300
+ if (isServe && !isVitest) {
301
+ const include = result.optimizeDeps?.include || [];
302
+ result.optimizeDeps = {
303
+ ...result.optimizeDeps,
304
+ exclude: [VIKE_CLIENT],
305
+ ...(sygnalInNodeModules(root) ? { include: [...include, 'sygnal'] } : {}),
306
+ };
196
307
  }
197
308
  return result;
198
309
  },
@@ -228,7 +339,7 @@ function sygnal(options = {}) {
228
339
  if (source === 'sygnal' && importer && wrapRun && runtimeImporters.has(cleanId(importer))) {
229
340
  return RUNTIME_ID;
230
341
  }
231
- if (source === VIKE_CLIENT && devOn && !opts?.ssr) {
342
+ if (source === VIKE_CLIENT && devInject && !opts?.ssr) {
232
343
  if (importer !== VIKE_CLIENT_ID)
233
344
  return VIKE_CLIENT_ID;
234
345
  // The wrapper's own import of the real entry: resolve it normally.
@@ -251,7 +362,9 @@ function sygnal(options = {}) {
251
362
  // pre-bundled copy of it can carry its own copy of the Sygnal core
252
363
  // (e.g. with a linked sygnal), which then holds the diagnostics bridge.
253
364
  if (id === VIKE_CLIENT_ID) {
254
- return `${devImports}${REINSTALL_IMPORT}\nexport * from '${VIKE_CLIENT}';\n${flags}\n${REINSTALL_CALL}`;
365
+ if (!devOn)
366
+ return `${devtoolsImport}\nexport * from '${VIKE_CLIENT}';\n`;
367
+ return `${devtoolsImport}${devImports}${REINSTALL_IMPORT}\nexport * from '${VIKE_CLIENT}';\n${flags}\n${REINSTALL_CALL}`;
255
368
  }
256
369
  return null;
257
370
  },
@@ -267,84 +380,176 @@ function sygnal(options = {}) {
267
380
  }, 0);
268
381
  },
269
382
  transform(code, id, opts) {
270
- if (!isServe)
271
- return null;
272
- // sygnal/astro/client (Astro islands): flags + checks first. It imports
273
- // the public 'sygnal' entry (the app's core, B-019), which resolves to
274
- // the run() wrapper when the diagnostics option needs it.
275
- if (devOn && !isVitest && !opts?.ssr) {
276
- astroClient = astroClient || astroClientFiles(root);
277
- if (astroClient.has(cleanId(id))) {
278
- if (wrapRun)
279
- runtimeImporters.add(cleanId(id));
280
- return withSourcemap(code, [[0, flags + devImports]], '', id);
281
- }
282
- }
283
- // Only transform JS/TS/JSX/TSX files
284
- if (!/\.[jt]sx?$/.test(id))
285
- return null;
286
- // Skip node_modules
287
- if (id.includes('node_modules'))
288
- return null;
289
- // Must import run from sygnal
290
- if (!code.includes('sygnal'))
291
- return null;
292
- // Match against the code with comments blanked out (same offsets), so
293
- // e.g. `// run(App)` or a commented-out import is never matched.
294
- const noComments = blankOut(code, false);
295
- // Find: import { run, ... } from 'sygnal'
296
- const runImportRe = /import\s+\{[^}]*\brun\b[^}]*\}\s+from\s+['"]sygnal['"]/;
297
- if (!runImportRe.test(noComments))
298
- return null;
299
- // Dev snippet: injected into every file that imports run() — also when
300
- // HMR wiring below is skipped. Inserted after any shebang line and
301
- // directive prologue, without adding a line. Nothing under Vitest: a
302
- // global flag set by one test file would leak dev/strict mode into the
303
- // later test files of the same worker; there the checks come from
304
- // test.setupFiles and renderComponent() manages the modes.
305
- const inserts = [];
306
- if (devOn && !isVitest) {
307
- inserts.push(flagInsertion(code, flags + devImports));
383
+ // SYG666 (PLAN-5 W-2): an adapter entry imported without its optional peers; every mode
384
+ const adapters = adapterImports(this, code, id);
385
+ if (adapters)
386
+ return checkPeers(this, adapters, id).then(() => devTransform.call(this, code, id, opts));
387
+ return devTransform.call(this, code, id, opts);
388
+ },
389
+ };
390
+ function devTransform(code, id, opts) {
391
+ if (!isServe)
392
+ return null;
393
+ // sygnal/astro/client (Astro islands): flags + checks first. It imports
394
+ // the public 'sygnal' entry (the app's core, B-019), which resolves to
395
+ // the run() wrapper when the diagnostics option needs it.
396
+ if (devInject && !isVitest && !opts?.ssr) {
397
+ astroClient = astroClient || astroClientFiles(root);
398
+ if (astroClient.has(cleanId(id))) {
308
399
  if (wrapRun)
309
400
  runtimeImporters.add(cleanId(id));
401
+ return withSourcemap(code, [[0, devSnippet]], '', id);
310
402
  }
311
- const done = (tail = '') => (inserts.length || tail ? withSourcemap(code, inserts, tail, id) : null);
312
- // Skip HMR wiring if disabled, in tests, or already manually wired
313
- if (disableHmr || isVitest || /\.(test|spec)\.[jt]sx?$/.test(id))
314
- return done();
315
- if (noComments.includes('import.meta.hot'))
316
- return done();
317
- // Strings and template literals blanked out too: a statement-level match only
318
- const runCall = RUN_CALL_RE.exec(blankOut(code, true));
319
- if (!runCall)
320
- return done();
321
- const [, binding, componentName] = runCall;
322
- // Find the import path for this component
323
- // Handles: import App from './App.jsx'
324
- // import App from "./App"
325
- const componentImportRe = new RegExp(`import\\s+${componentName.replace(/\$/g, '\\$')}\\s+from\\s+['"]([^'"]+)['"]`);
326
- const componentImportMatch = noComments.match(componentImportRe);
327
- if (!componentImportMatch)
328
- return done();
329
- const componentPath = componentImportMatch[1];
330
- // Pattern 3: bare run(App, ...) — no result captured → const __sygnal = run(
331
- if (!binding) {
332
- inserts.push([runCall.index, 'const __sygnal = ']);
333
- return done(hmrBlock(componentPath, '__sygnal.hmr', '__sygnal.dispose'));
334
- }
335
- // Pattern 2: const app = run(App, ...)
336
- if (binding[0] !== '{') {
337
- return done(hmrBlock(componentPath, `${binding}.hmr`, `${binding}.dispose`));
403
+ }
404
+ // Only transform JS/TS/JSX/TSX files
405
+ if (!/\.[jt]sx?$/.test(id))
406
+ return null;
407
+ // Skip node_modules
408
+ if (id.includes('node_modules'))
409
+ return null;
410
+ // Must import run from sygnal
411
+ if (!code.includes('sygnal'))
412
+ return null;
413
+ // Match against the code with comments blanked out (same offsets), so
414
+ // e.g. `// run(App)` or a commented-out import is never matched.
415
+ const noComments = blankOut(code, false);
416
+ // Find: import { run, ... } from 'sygnal'
417
+ const runImportRe = /import\s+\{[^}]*\brun\b[^}]*\}\s+from\s+['"]sygnal['"]/;
418
+ if (!runImportRe.test(noComments)) {
419
+ return disableHmr || isVitest ? null : elementHmr(code, noComments, id);
420
+ }
421
+ // Dev snippet: injected into every file that imports run() — also when
422
+ // HMR wiring below is skipped. Inserted after any shebang line and
423
+ // directive prologue, without adding a line. Nothing under Vitest: a
424
+ // global flag set by one test file would leak dev/strict mode into the
425
+ // later test files of the same worker; there the checks come from
426
+ // test.setupFiles and renderComponent() manages the modes.
427
+ const inserts = [];
428
+ if (devInject && !isVitest) {
429
+ inserts.push(flagInsertion(code, devSnippet));
430
+ if (wrapRun)
431
+ runtimeImporters.add(cleanId(id));
432
+ }
433
+ const done = (tail = '') => (inserts.length || tail ? withSourcemap(code, inserts, tail, id) : null);
434
+ // Skip HMR wiring if disabled, in tests, or already manually wired
435
+ if (disableHmr || isVitest || /\.(test|spec)\.[jt]sx?$/.test(id))
436
+ return done();
437
+ if (noComments.includes('import.meta.hot'))
438
+ return done();
439
+ // Strings and template literals blanked out too: a statement-level match only
440
+ const runCall = RUN_CALL_RE.exec(blankOut(code, true));
441
+ if (!runCall)
442
+ return done();
443
+ const [, binding, componentName] = runCall;
444
+ // Find the import path for this component
445
+ // Handles: import App from './App.jsx'
446
+ // import App from "./App"
447
+ const componentImportRe = new RegExp(`import\\s+${componentName.replace(/\$/g, '\\$')}\\s+from\\s+['"]([^'"]+)['"]`);
448
+ const componentImportMatch = noComments.match(componentImportRe);
449
+ if (!componentImportMatch)
450
+ return done();
451
+ const componentPath = componentImportMatch[1];
452
+ // Pattern 3: bare run(App, ...) — no result captured → const __sygnal = run(
453
+ if (!binding) {
454
+ inserts.push([runCall.index, 'const __sygnal = ']);
455
+ return done(hmrBlock(componentPath, '__sygnal.hmr', '__sygnal.dispose'));
456
+ }
457
+ // Pattern 2: const app = run(App, ...)
458
+ if (binding[0] !== '{') {
459
+ return done(hmrBlock(componentPath, `${binding}.hmr`, `${binding}.dispose`));
460
+ }
461
+ // Pattern 1: const { hmr, dispose, ... } = run(App, ...) — only when both
462
+ // are bound under their own names (a rename would leave them undefined)
463
+ const names = binding.slice(1, -1).split(',').map(s => s.trim());
464
+ if (names.includes('hmr') && names.includes('dispose')) {
465
+ return done(hmrBlock(componentPath, 'hmr', 'dispose'));
466
+ }
467
+ return done();
468
+ }
469
+ }
470
+ // PLAN-5 W-2 (D209): the adapter entries and the optional peer dependencies each one imports
471
+ // (the Zag packages pinned together: they depend on each other's exact versions)
472
+ const ZAG = '~1.45.0';
473
+ const ADAPTER_PEERS = {
474
+ react: ['react', 'react-dom'],
475
+ zag: ['@zag-js/vanilla'],
476
+ // D211: one subpath per Zag part, each needing only its own machine
477
+ 'ui/menu': ['@zag-js/vanilla', '@zag-js/menu'],
478
+ 'ui/select': ['@zag-js/vanilla', '@zag-js/select'],
479
+ 'ui/combobox': ['@zag-js/vanilla', '@zag-js/combobox'],
480
+ };
481
+ // G-440: a dynamic import's specifier may be a template literal without substitutions
482
+ const ADAPTER_RE = /(?:\bfrom|\bimport)\s*\(?\s*(['"`])sygnal\/(react|zag|ui\/(?:menu|select|combobox))\1/g;
483
+ const ADAPTER_ID = /^sygnal\/(react|zag|ui\/(?:menu|select|combobox))$/;
484
+ /**
485
+ * The adapter entries an app module imports (statically, dynamically or in a re-export), or null.
486
+ * G-415: real imports only. The module's AST from the plugin context's parse() (Vite and Rollup
487
+ * provide it; the code is plain JS by then); when there is none, or the code doesn't parse, a
488
+ * scan of the code with comments blanked whose `from` / `import` keyword isn't inside a string
489
+ * or template literal. A literal that mentions an entry (a docs snippet) is never an import.
490
+ */
491
+ function adapterImports(ctx, code, id) {
492
+ if (!code.includes('sygnal/') || id.includes('node_modules') || !/\.[cm]?[jt]sx?$/.test(cleanId(id)))
493
+ return null;
494
+ const noComments = blankOut(code, false);
495
+ if (!ADAPTER_RE.test(noComments))
496
+ return null;
497
+ ADAPTER_RE.lastIndex = 0;
498
+ const found = new Set();
499
+ let ast = null;
500
+ try {
501
+ ast = typeof ctx?.parse == 'function' ? ctx.parse(code) : null;
502
+ }
503
+ catch (_) { }
504
+ if (ast) {
505
+ const walk = (n) => {
506
+ if (!n || typeof n != 'object')
507
+ return;
508
+ if (Array.isArray(n))
509
+ return n.forEach(walk);
510
+ if (/^(Import|Export(Named|All))Declaration$|^ImportExpression$/.test(n.type)) {
511
+ // G-440: import(`sygnal/zag`): a template literal without substitutions is a known specifier
512
+ const src = n.source, q = src?.type == 'TemplateLiteral' && !src.expressions?.length ? src.quasis?.[0]?.value?.cooked : src?.value;
513
+ const m = ADAPTER_ID.exec(q ?? '');
514
+ if (m)
515
+ found.add(m[1]);
338
516
  }
339
- // Pattern 1: const { hmr, dispose, ... } = run(App, ...) — only when both
340
- // are bound under their own names (a rename would leave them undefined)
341
- const names = binding.slice(1, -1).split(',').map(s => s.trim());
342
- if (names.includes('hmr') && names.includes('dispose')) {
343
- return done(hmrBlock(componentPath, 'hmr', 'dispose'));
517
+ for (const k in n)
518
+ if (k != 'type' && typeof n[k] == 'object')
519
+ walk(n[k]);
520
+ };
521
+ walk(ast);
522
+ }
523
+ else {
524
+ // strings and templates blanked too: a keyword there is text, not an import
525
+ const noStrings = blankOut(code, true);
526
+ for (const m of noComments.matchAll(ADAPTER_RE)) {
527
+ if (noStrings.startsWith(m[0].slice(0, m[0][0] == 'f' ? 4 : 6), m.index))
528
+ found.add(m[2]);
529
+ }
530
+ }
531
+ return found.size ? [...found] : null;
532
+ }
533
+ /** SYG666: resolves each entry's peers from the importing module; reports the missing ones */
534
+ async function checkPeers(ctx, entries, id) {
535
+ for (const e of entries) {
536
+ const missing = [];
537
+ for (const peer of ADAPTER_PEERS[e]) {
538
+ let r = null;
539
+ try {
540
+ r = await ctx.resolve(peer, id, { skipSelf: true });
344
541
  }
345
- return done();
346
- },
347
- };
542
+ catch (_) { }
543
+ if (!r)
544
+ missing.push(peer);
545
+ }
546
+ if (!missing.length)
547
+ continue;
548
+ const install = missing.map(m => (m.startsWith('@zag-js/') ? `${m}@${ZAG}` : m)).join(' ');
549
+ ctx.error(`[Sygnal SYG666] 'sygnal/${e}' needs ${missing.join(', ')}, which ${missing.length > 1 ? "aren't" : "isn't"} installed: npm install ${install}.` +
550
+ (e == 'react' ? " (For Preact: alias react, react-dom and react-dom/client to preact/compat.)" : '') +
551
+ ' https://sygnal.js.org/reference/errors#syg666');
552
+ }
348
553
  }
349
554
  /**
350
555
  * Major version of the running Vite: from the plugin context (this.meta,
@@ -547,6 +752,8 @@ async function startChecker(server, root, opts, defaults, explicit) {
547
752
  const include = checkInclude(root, opts.include, (m) => logger.info(m));
548
753
  const ignore = opts.ignore || defaults.ignore;
549
754
  const strict = opts.strict === undefined ? defaults.strict : !!opts.strict;
755
+ // D144: the a11y lane is a warning unless asked for as an error, under strict too
756
+ const a11y = opts.a11y === 'error' ? 'error' : 'warn';
550
757
  // Only error-severity findings open Vite's overlay: while an overlay is open
551
758
  // Vite's client reloads the page on the first HMR update, so warnings stay
552
759
  // in the terminal and the browser console. overlay: 'warn' is treated as
@@ -577,12 +784,16 @@ async function startChecker(server, root, opts, defaults, explicit) {
577
784
  const run = (initial = false) => {
578
785
  let diags;
579
786
  try {
580
- diags = mod.check(include, { cwd: root, strict, ignore });
787
+ diags = mod.check(include, { cwd: root, strict, ignore, a11y });
581
788
  }
582
789
  catch (err) {
583
790
  logger.warn(`[sygnal] sygnal-check failed: ${err?.message || err}`, { timestamp: true });
584
791
  return;
585
792
  }
793
+ // A sygnal-check from before D144 reports SYG7xx as errors under strict: keep them warnings
794
+ if (a11y !== 'error') {
795
+ diags = diags.map(d => d && d.severity === 'error' && /^SYG7\d\d$/.test(d.code) ? { ...d, severity: 'warn' } : d);
796
+ }
586
797
  const shown = diags.filter(d => d.severity !== 'info');
587
798
  const lines = shown.map(formatLine);
588
799
  const key = lines.join('\n');
@@ -735,6 +946,29 @@ function withSourcemap(code, inserts, tail, id) {
735
946
  map: { version: 3, sources: [id], sourcesContent: [code], names: [], mappings },
736
947
  };
737
948
  }
949
+ // A top-level (column 0) defineElement(...) call, as a statement or a declaration's value
950
+ const DEFINE_ELEMENT_CALL_RE = /^(?:(?:export\s+)?(?:const|let|var)\s+[\w$]+\s*=\s*)?defineElement\s*\(/m;
951
+ const ELEMENT_IMPORT_RE = /import\s+\{[^}]*\bdefineElement\b[^}]*\}\s+from\s+['"]sygnal\/element['"]/;
952
+ /**
953
+ * PLAN-4 GS-13: a module that defines custom elements with 'sygnal/element' accepts its own
954
+ * hot updates. Re-running it calls defineElement again for the same tags, which swaps the
955
+ * component in every live element and keeps each one's state; an edited component module it
956
+ * imports reaches it the same way. Only for a top-level defineElement call, outside test
957
+ * files, when the module has no HMR code of its own and doesn't import run() (an entry's
958
+ * run() wiring decides there).
959
+ */
960
+ function elementHmr(code, noComments, id) {
961
+ if (/\.(test|spec)\.[jt]sx?$/.test(id))
962
+ return null;
963
+ if (!ELEMENT_IMPORT_RE.test(noComments) || noComments.includes('import.meta.hot'))
964
+ return null;
965
+ if (!DEFINE_ELEMENT_CALL_RE.test(blankOut(code, true)))
966
+ return null;
967
+ return withSourcemap(code, [], ELEMENT_HMR_BLOCK, id);
968
+ }
969
+ const ELEMENT_HMR_BLOCK = `
970
+ if (import.meta.hot) import.meta.hot.accept()
971
+ `;
738
972
  function hmrBlock(componentPath, hmrRef, disposeRef) {
739
973
  return `
740
974
  if (import.meta.hot) {
@@ -745,11 +979,32 @@ if (import.meta.hot) {
745
979
  }
746
980
 
747
981
  const SYGNAL_RENDERER_NAME = '@sygnal/astro';
982
+ // PLAN-4 GS-11 / D120: both island entries import this; it re-exports the default export of the
983
+ // module the `onError` option names (undefined without it)
984
+ const ON_ERROR_ID = 'virtual:sygnal/astro-on-error';
985
+ /** The Vite plugin serving ON_ERROR_ID; `file` is absolute (undefined: no hook) */
986
+ function onErrorPlugin(file) {
987
+ return {
988
+ name: 'sygnal:astro-on-error',
989
+ resolveId: (id) => id === ON_ERROR_ID ? '\0' + ON_ERROR_ID : undefined,
990
+ load: (id) => id === '\0' + ON_ERROR_ID
991
+ ? (file ? `export { default } from ${JSON.stringify(file)}` : 'export default undefined')
992
+ : undefined,
993
+ // Vite 6+ (Astro 6 renders in its own 'prerender' / 'ssr' environments): the island server
994
+ // entry imports the virtual module, so Vite must load it (not Node). `ssr.noExternal`
995
+ // below covers the 'ssr' environment and older Vite
996
+ configEnvironment: (name) => name === 'client' ? undefined : { resolve: { noExternal: ['sygnal'] } },
997
+ };
998
+ }
999
+ /** The project root as a directory path (Astro gives a file: URL) */
1000
+ const rootDir = (root) =>
1001
+ // @ts-ignore — Node's process
1002
+ !root ? process.cwd() : typeof root === 'string' ? root : fileURLToPath(root);
748
1003
  function sygnalAstroIntegration(options = {}) {
749
1004
  return {
750
1005
  name: SYGNAL_RENDERER_NAME,
751
1006
  hooks: {
752
- 'astro:config:setup': ({ addRenderer, updateConfig, command }) => {
1007
+ 'astro:config:setup': ({ addRenderer, updateConfig, command, config }) => {
753
1008
  addRenderer({
754
1009
  name: SYGNAL_RENDERER_NAME,
755
1010
  clientEntrypoint: 'sygnal/astro/client',
@@ -760,11 +1015,16 @@ function sygnalAstroIntegration(options = {}) {
760
1015
  jsx: 'automatic',
761
1016
  jsxImportSource: 'sygnal',
762
1017
  },
1018
+ // D120: the island entries import the virtual onError module, so they stay on Vite:
1019
+ // not pre-bundled with it, and not externalized (loaded by Node) during SSR
1020
+ optimizeDeps: { exclude: [ON_ERROR_ID] },
1021
+ ssr: { noExternal: ['sygnal'] },
763
1022
  };
1023
+ const hook = options.onError ? path.resolve(rootDir(config?.root), options.onError) : undefined;
764
1024
  // Dev mode (G-014): islands are started by sygnal/astro/client, which
765
1025
  // user code never imports, so the sygnal Vite plugin wraps that entry
766
- // in dev to set the dev flag and load the runtime checks first. It
767
- // also runs sygnal-check. Nothing is added to `astro build`.
1026
+ // in dev to install DevTools, set the dev flag and load the runtime checks first. It
1027
+ // also runs sygnal-check. `astro build` only gets the `globalthis` alias.
768
1028
  if (command === 'dev') {
769
1029
  vite.plugins = [sygnal({
770
1030
  disableJsx: true,
@@ -772,8 +1032,18 @@ function sygnalAstroIntegration(options = {}) {
772
1032
  vitestSetup: false,
773
1033
  diagnostics: options.diagnostics,
774
1034
  check: options.check,
1035
+ nativeGlobalThis: options.nativeGlobalThis,
1036
+ devtools: options.devtools,
775
1037
  })];
776
1038
  }
1039
+ else if (options.nativeGlobalThis !== false) {
1040
+ // G-099: the same `globalthis` alias the plugin adds in dev (R2-7: unless the
1041
+ // user's vite config already aliases `globalthis`)
1042
+ const alias = globalThisAlias(config?.vite?.resolve?.alias);
1043
+ if (alias.length)
1044
+ vite.resolve = { alias };
1045
+ }
1046
+ vite.plugins = [...(vite.plugins || []), onErrorPlugin(hook)];
777
1047
  updateConfig({ vite });
778
1048
  },
779
1049
  },