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