sygnal 5.4.0 → 6.1.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 (360) hide show
  1. package/CHANGELOG.md +716 -0
  2. package/README.md +24 -9
  3. package/dist/ai.cjs.js +115 -0
  4. package/dist/ai.cjs.js.map +1 -0
  5. package/dist/ai.esm.js +2 -0
  6. package/dist/ai.esm.js.map +1 -0
  7. package/dist/astro/client.cjs.js +16 -9
  8. package/dist/astro/client.cjs.js.map +1 -1
  9. package/dist/astro/client.mjs +16 -9
  10. package/dist/astro/client.mjs.map +1 -1
  11. package/dist/astro/index.cjs.js +958 -92
  12. package/dist/astro/index.cjs.js.map +1 -1
  13. package/dist/astro/index.mjs +958 -93
  14. package/dist/astro/index.mjs.map +1 -1
  15. package/dist/astro/server.cjs.js +3396 -165
  16. package/dist/astro/server.cjs.js.map +1 -1
  17. package/dist/astro/server.mjs +3396 -165
  18. package/dist/astro/server.mjs.map +1 -1
  19. package/dist/devtools.cjs.js +1840 -0
  20. package/dist/devtools.cjs.js.map +1 -0
  21. package/dist/devtools.esm.js +1827 -0
  22. package/dist/devtools.esm.js.map +1 -0
  23. package/dist/diagnostics.cjs.js +3190 -296
  24. package/dist/diagnostics.cjs.js.map +1 -1
  25. package/dist/diagnostics.esm.js +3190 -296
  26. package/dist/diagnostics.esm.js.map +1 -1
  27. package/dist/element.cjs.js +230 -0
  28. package/dist/element.cjs.js.map +1 -0
  29. package/dist/element.esm.js +228 -0
  30. package/dist/element.esm.js.map +1 -0
  31. package/dist/guide/accessibility.md +310 -0
  32. package/dist/guide/adapters.md +221 -0
  33. package/dist/guide/agent.md +588 -0
  34. package/dist/guide/ai-chat.md +693 -0
  35. package/dist/guide/ai-decisions.md +330 -0
  36. package/dist/guide/behaviors.md +312 -0
  37. package/dist/guide/browser-sources.md +203 -0
  38. package/dist/guide/drag-and-drop.md +342 -0
  39. package/dist/guide/element-commands.md +306 -0
  40. package/dist/guide/error-boundaries.md +169 -0
  41. package/dist/guide/forms-reference.md +409 -0
  42. package/dist/guide/forms.md +251 -0
  43. package/dist/guide/http.md +301 -0
  44. package/dist/guide/inputs.md +173 -0
  45. package/dist/guide/mcp-apps.md +272 -0
  46. package/dist/guide/persistence.md +245 -0
  47. package/dist/guide/recipes/ai-form-fill.md +163 -0
  48. package/dist/guide/recipes/ai-summarize.md +155 -0
  49. package/dist/guide/recipes/ai-support-inbox.md +190 -0
  50. package/dist/guide/recipes/carousel.md +155 -0
  51. package/dist/guide/recipes/charts.md +177 -0
  52. package/dist/guide/recipes/code-editor.md +135 -0
  53. package/dist/guide/recipes/data-grid.md +155 -0
  54. package/dist/guide/recipes/data-table.md +196 -0
  55. package/dist/guide/recipes/i18n.md +222 -0
  56. package/dist/guide/recipes/icons.md +115 -0
  57. package/dist/guide/recipes/overview.md +43 -0
  58. package/dist/guide/recipes/rich-text.md +154 -0
  59. package/dist/guide/resources.md +471 -0
  60. package/dist/guide/ssr.md +255 -0
  61. package/dist/guide/timers.md +178 -0
  62. package/dist/guide/ui/accordion.md +93 -0
  63. package/dist/guide/ui/combobox.md +144 -0
  64. package/dist/guide/ui/dialog.md +124 -0
  65. package/dist/guide/ui/disclosure.md +60 -0
  66. package/dist/guide/ui/menu.md +125 -0
  67. package/dist/guide/ui/overview.md +166 -0
  68. package/dist/guide/ui/popover.md +101 -0
  69. package/dist/guide/ui/select.md +114 -0
  70. package/dist/guide/ui/tabs.md +161 -0
  71. package/dist/guide/ui/toaster.md +152 -0
  72. package/dist/guide/ui/tooltip.md +103 -0
  73. package/dist/guide/undo.md +178 -0
  74. package/dist/guide/virtual-collections.md +183 -0
  75. package/dist/guide/web-components.md +179 -0
  76. package/dist/guide/webmcp.md +226 -0
  77. package/dist/guide/widgets.md +217 -0
  78. package/dist/index.cjs.js +19288 -7408
  79. package/dist/index.cjs.js.map +1 -1
  80. package/dist/index.d.ts +4466 -552
  81. package/dist/index.esm.js +19274 -7461
  82. package/dist/index.esm.js.map +1 -1
  83. package/dist/jsx-dev-runtime.cjs.js +26 -334
  84. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  85. package/dist/jsx-dev-runtime.esm.js +19 -327
  86. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  87. package/dist/jsx-runtime.cjs.js +26 -334
  88. package/dist/jsx-runtime.cjs.js.map +1 -1
  89. package/dist/jsx-runtime.esm.js +19 -327
  90. package/dist/jsx-runtime.esm.js.map +1 -1
  91. package/dist/jsx.cjs.js +10 -314
  92. package/dist/jsx.cjs.js.map +1 -1
  93. package/dist/jsx.esm.js +8 -315
  94. package/dist/jsx.esm.js.map +1 -1
  95. package/dist/react.cjs.js +117 -0
  96. package/dist/react.cjs.js.map +1 -0
  97. package/dist/react.esm.js +115 -0
  98. package/dist/react.esm.js.map +1 -0
  99. package/dist/shims/globalthis.cjs +20 -0
  100. package/dist/sygnal.min.js +1 -1
  101. package/dist/sygnal.min.js.map +1 -1
  102. package/dist/ui-combobox.cjs.js +169 -0
  103. package/dist/ui-combobox.cjs.js.map +1 -0
  104. package/dist/ui-combobox.esm.js +148 -0
  105. package/dist/ui-combobox.esm.js.map +1 -0
  106. package/dist/ui-menu.cjs.js +87 -0
  107. package/dist/ui-menu.cjs.js.map +1 -0
  108. package/dist/ui-menu.esm.js +66 -0
  109. package/dist/ui-menu.esm.js.map +1 -0
  110. package/dist/ui-select.cjs.js +104 -0
  111. package/dist/ui-select.cjs.js.map +1 -0
  112. package/dist/ui-select.esm.js +83 -0
  113. package/dist/ui-select.esm.js.map +1 -0
  114. package/dist/ui.cjs.js +738 -0
  115. package/dist/ui.cjs.js.map +1 -0
  116. package/dist/ui.esm.js +727 -0
  117. package/dist/ui.esm.js.map +1 -0
  118. package/dist/vike/ClientOnly.cjs.js +4 -14
  119. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  120. package/dist/vike/ClientOnly.mjs +4 -14
  121. package/dist/vike/ClientOnly.mjs.map +1 -1
  122. package/dist/vike/{+config.js → config/+config.js} +9 -1
  123. package/dist/vike/config/+config.js.map +1 -0
  124. package/dist/vike/config/package.json +6 -0
  125. package/dist/vike/onRenderClient.cjs.js +165 -95
  126. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  127. package/dist/vike/onRenderClient.mjs +165 -95
  128. package/dist/vike/onRenderClient.mjs.map +1 -1
  129. package/dist/vike/onRenderHtml.cjs.js +59 -24
  130. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  131. package/dist/vike/onRenderHtml.mjs +60 -25
  132. package/dist/vike/onRenderHtml.mjs.map +1 -1
  133. package/dist/vite/plugin.cjs.js +919 -89
  134. package/dist/vite/plugin.cjs.js.map +1 -1
  135. package/dist/vite/plugin.mjs +919 -90
  136. package/dist/vite/plugin.mjs.map +1 -1
  137. package/dist/zag.cjs.js +408 -0
  138. package/dist/zag.cjs.js.map +1 -0
  139. package/dist/zag.esm.js +405 -0
  140. package/dist/zag.esm.js.map +1 -0
  141. package/llms.txt +172 -103
  142. package/package.json +112 -14
  143. package/src/ai.d.ts +1186 -0
  144. package/src/ai.ts +23 -0
  145. package/src/astro/client.ts +27 -16
  146. package/src/astro/index.d.ts +24 -0
  147. package/src/astro/index.ts +69 -3
  148. package/src/astro/server.ts +8 -2
  149. package/src/collection.ts +6 -93
  150. package/src/core/actions.ts +134 -0
  151. package/src/core/cell.ts +202 -0
  152. package/src/core/debug.ts +23 -0
  153. package/src/core/define.ts +242 -0
  154. package/src/core/hooks.ts +314 -0
  155. package/src/core/hosts/collection.ts +325 -0
  156. package/src/core/hosts/switchable.ts +146 -0
  157. package/src/core/instance.ts +592 -0
  158. package/src/core/markers/clientonly.ts +13 -0
  159. package/src/core/markers/lazy.ts +40 -0
  160. package/src/core/markers/portal.ts +96 -0
  161. package/src/core/markers/suspense.ts +41 -0
  162. package/src/core/markers/transition.ts +70 -0
  163. package/src/core/registry.ts +25 -0
  164. package/src/core/runtime.ts +573 -0
  165. package/src/core/statics.ts +104 -0
  166. package/src/core/teardown.ts +60 -0
  167. package/src/core/view.ts +37 -0
  168. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  169. package/src/cycle/dom/ElementFinder.ts +7 -9
  170. package/src/cycle/dom/EventDelegator.ts +174 -223
  171. package/src/cycle/dom/IsolateModule.ts +65 -35
  172. package/src/cycle/dom/MainDOMSource.ts +9 -2
  173. package/src/cycle/dom/PriorityQueue.ts +4 -2
  174. package/src/cycle/dom/SymbolTree.ts +18 -47
  175. package/src/cycle/dom/classNameModule.ts +9 -7
  176. package/src/cycle/dom/controlledInputModule.ts +23 -6
  177. package/src/cycle/dom/enrichEventStream.ts +25 -47
  178. package/src/cycle/dom/fragment.ts +7 -0
  179. package/src/cycle/dom/isolate.ts +30 -27
  180. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  181. package/src/cycle/dom/mockDOMSource.ts +21 -1
  182. package/src/cycle/dom/modules.ts +16 -3
  183. package/src/cycle/dom/propsModule.ts +65 -0
  184. package/src/cycle/dom/selectModule.ts +22 -17
  185. package/src/cycle/dom/snabbdom.ts +1 -6
  186. package/src/cycle/dom/thunk.ts +3 -0
  187. package/src/cycle/dom/utils.ts +98 -0
  188. package/src/cycle/dom/viewTransition.ts +43 -0
  189. package/src/cycle/state/StateSource.ts +19 -3
  190. package/src/cycle/state/objIsEqual.ts +50 -0
  191. package/src/cycle/state/types.ts +0 -10
  192. package/src/defineComponent.ts +34 -0
  193. package/src/devtools.d.ts +170 -0
  194. package/src/devtools.ts +37 -0
  195. package/src/element.d.ts +59 -0
  196. package/src/element.ts +235 -0
  197. package/src/extra/ai/agent/index.ts +484 -0
  198. package/src/extra/ai/answers.ts +115 -0
  199. package/src/extra/ai/chat/behavior.ts +336 -0
  200. package/src/extra/ai/chat/driver.ts +271 -0
  201. package/src/extra/ai/chat/memoryTransport.ts +61 -0
  202. package/src/extra/ai/chat/output.ts +37 -0
  203. package/src/extra/ai/commandBar.ts +281 -0
  204. package/src/extra/ai/decide.ts +104 -0
  205. package/src/extra/ai/index.ts +42 -0
  206. package/src/extra/ai/link.ts +90 -0
  207. package/src/extra/ai/mcpApp.ts +314 -0
  208. package/src/extra/ai/messages.ts +51 -0
  209. package/src/extra/ai/schema/index.ts +166 -0
  210. package/src/extra/ai/schema/jsonSchema.ts +72 -0
  211. package/src/extra/ai/schema/strict.ts +152 -0
  212. package/src/extra/ai/transports/agui.ts +177 -0
  213. package/src/extra/ai/transports/anthropicMessages.ts +175 -0
  214. package/src/extra/ai/transports/chatCompletions.ts +105 -0
  215. package/src/extra/ai/transports/chromePrompt.ts +77 -0
  216. package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
  217. package/src/extra/ai/transports/fromAISDK.ts +126 -0
  218. package/src/extra/ai/transports/openResponses.ts +105 -0
  219. package/src/extra/ai/transports/shared.ts +125 -0
  220. package/src/extra/ai/transports/tools.ts +59 -0
  221. package/src/extra/ai/transports/uiMessageStream.ts +107 -0
  222. package/src/extra/ai/webmcp.ts +260 -0
  223. package/src/extra/backoff.ts +9 -0
  224. package/src/extra/behaviors.ts +169 -0
  225. package/src/extra/browserSignals.ts +23 -0
  226. package/src/extra/browserSources.ts +326 -0
  227. package/src/extra/command.ts +3 -3
  228. package/src/extra/controls.ts +47 -0
  229. package/src/extra/copyAsTest.ts +286 -0
  230. package/src/extra/devMcp.ts +386 -0
  231. package/src/extra/devtools.ts +101 -31
  232. package/src/extra/devtoolsActions.ts +379 -0
  233. package/src/extra/devtoolsHook.ts +9 -0
  234. package/src/extra/devtoolsNext.ts +83 -0
  235. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  236. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  237. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  238. package/src/extra/diagnostics/checks/chat.ts +71 -0
  239. package/src/extra/diagnostics/checks/collections.ts +1 -1
  240. package/src/extra/diagnostics/checks/controls.ts +148 -0
  241. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  242. package/src/extra/diagnostics/checks/dom.ts +20 -11
  243. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  244. package/src/extra/diagnostics/checks/events.ts +17 -2
  245. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  246. package/src/extra/diagnostics/checks/forms.ts +171 -0
  247. package/src/extra/diagnostics/checks/index.ts +103 -4
  248. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  249. package/src/extra/diagnostics/checks/next.ts +417 -0
  250. package/src/extra/diagnostics/checks/persist.ts +54 -0
  251. package/src/extra/diagnostics/checks/props.ts +10 -5
  252. package/src/extra/diagnostics/checks/public.d.ts +115 -7
  253. package/src/extra/diagnostics/checks/replies.ts +128 -0
  254. package/src/extra/diagnostics/checks/router.ts +100 -0
  255. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  256. package/src/extra/diagnostics/checks/shared.ts +86 -2
  257. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  258. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  259. package/src/extra/diagnostics/checks/state.ts +100 -7
  260. package/src/extra/diagnostics/checks/statics.ts +48 -0
  261. package/src/extra/diagnostics/checks/strict.ts +10 -67
  262. package/src/extra/diagnostics/checks/timers.ts +80 -0
  263. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  264. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  265. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  266. package/src/extra/diagnostics/checks/wiring.ts +69 -10
  267. package/src/extra/diagnostics/codes.ts +335 -23
  268. package/src/extra/diagnostics/index.ts +34 -9
  269. package/src/extra/diagnostics/legacy.ts +17 -0
  270. package/src/extra/driverFactories.ts +83 -19
  271. package/src/extra/elementCommands.ts +53 -0
  272. package/src/extra/fetchDriver.ts +519 -0
  273. package/src/extra/focusWithin.ts +27 -0
  274. package/src/extra/form.ts +284 -0
  275. package/src/extra/formHelpers.ts +139 -0
  276. package/src/extra/formTool.ts +150 -0
  277. package/src/extra/head.ts +106 -0
  278. package/src/extra/hmr.ts +2 -3
  279. package/src/extra/owned.ts +24 -0
  280. package/src/extra/pager.ts +50 -0
  281. package/src/extra/persist.ts +133 -0
  282. package/src/extra/pwa.ts +1 -1
  283. package/src/extra/queryCache.ts +107 -0
  284. package/src/extra/reducers.ts +1 -1
  285. package/src/extra/reduxDevtools.ts +92 -0
  286. package/src/extra/replies.ts +54 -0
  287. package/src/extra/router.ts +323 -0
  288. package/src/extra/run.ts +90 -217
  289. package/src/extra/selection.ts +76 -0
  290. package/src/extra/socketDriver.ts +265 -0
  291. package/src/extra/sortable.ts +377 -0
  292. package/src/extra/ssr.ts +427 -160
  293. package/src/extra/standardSchema.ts +27 -0
  294. package/src/extra/testing.ts +2550 -175
  295. package/src/extra/timers.ts +95 -0
  296. package/src/extra/undo.ts +261 -0
  297. package/src/extra/viewTransitions.ts +38 -0
  298. package/src/extra/virtual.ts +513 -0
  299. package/src/extra/widget.ts +201 -0
  300. package/src/index.d.ts +2754 -127
  301. package/src/index.ts +45 -4
  302. package/src/jsx-runtime.ts +15 -1
  303. package/src/jsx.ts +2 -1
  304. package/src/lazy.ts +50 -7
  305. package/src/portal.ts +3 -1
  306. package/src/pragma/index.ts +256 -133
  307. package/src/react-peers.d.ts +5 -0
  308. package/src/react.d.ts +43 -0
  309. package/src/react.ts +110 -0
  310. package/src/shared.ts +57 -0
  311. package/src/slot.ts +1 -1
  312. package/src/suspense.ts +3 -1
  313. package/src/switchable.ts +6 -121
  314. package/src/transition.ts +3 -1
  315. package/src/ui/accordion.ts +64 -0
  316. package/src/ui/dialog.ts +129 -0
  317. package/src/ui/disclosure.ts +35 -0
  318. package/src/ui/popover.ts +45 -0
  319. package/src/ui/shared.ts +94 -0
  320. package/src/ui/tabs.ts +82 -0
  321. package/src/ui/toaster.ts +198 -0
  322. package/src/ui/tooltip.ts +70 -0
  323. package/src/ui/zag/combobox.ts +115 -0
  324. package/src/ui/zag/menu.ts +40 -0
  325. package/src/ui/zag/select.ts +54 -0
  326. package/src/ui/zag/shared.ts +43 -0
  327. package/src/ui-combobox.d.ts +20 -0
  328. package/src/ui-combobox.ts +6 -0
  329. package/src/ui-menu.d.ts +30 -0
  330. package/src/ui-menu.ts +6 -0
  331. package/src/ui-select.d.ts +17 -0
  332. package/src/ui-select.ts +6 -0
  333. package/src/ui-zag-types.d.ts +42 -0
  334. package/src/ui.d.ts +208 -0
  335. package/src/ui.ts +15 -0
  336. package/src/vike/+config.ts +9 -1
  337. package/src/vike/ClientOnly.ts +1 -1
  338. package/src/vike/onRenderClient.ts +144 -96
  339. package/src/vike/onRenderHtml.ts +69 -26
  340. package/src/vike/types.ts +7 -1
  341. package/src/vite/globalthis-shim.ts +18 -0
  342. package/src/vite/globalthis.ts +56 -0
  343. package/src/vite/mcp.ts +568 -0
  344. package/src/vite/plugin.d.ts +47 -3
  345. package/src/vite/plugin.ts +262 -25
  346. package/src/zag.d.ts +64 -0
  347. package/src/zag.ts +238 -0
  348. package/dist/vike/+config.cjs.js +0 -66
  349. package/dist/vike/+config.cjs.js.map +0 -1
  350. package/dist/vike/+config.js.map +0 -1
  351. package/src/component.ts +0 -2301
  352. package/src/cycle/isolate/index.ts +0 -196
  353. package/src/cycle/run/index.ts +0 -151
  354. package/src/cycle/run/internals.ts +0 -143
  355. package/src/cycle/state/Collection.ts +0 -184
  356. package/src/cycle/state/index.ts +0 -5
  357. package/src/cycle/state/pickCombine.ts +0 -201
  358. package/src/cycle/state/pickMerge.ts +0 -122
  359. package/src/cycle/state/withState.ts +0 -47
  360. package/src/pragma/fn.ts +0 -55
@@ -5,6 +5,624 @@ 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
+
61
+ /**
62
+ * PLAN-6 E-1: the dev MCP endpoint of sygnal/vite (`sygnal({ mcp: true })`), dev server only.
63
+ *
64
+ * `/__sygnal/mcp` speaks MCP's streamable HTTP transport: JSON-RPC 2.0 over POST, answered as
65
+ * one `application/json` body, in both protocol eras (G-655: a dual-era server):
66
+ *
67
+ * - legacy, the `initialize` revisions (2024-11-05 … 2025-11-25). G-638: JSON replies make a
68
+ * complete server under the spec (the client must accept JSON or SSE; a server without a GET
69
+ * stream answers 405; session ids are optional, "MAY assign"). Methods: initialize, ping,
70
+ * tools/list, tools/call; notifications get 202; batches (2025-03-26) are answered.
71
+ * - modern, the stateless 2026-07-28 revision (no initialize, no sessions, no ping): each request
72
+ * carries `_meta['io.modelcontextprotocol/protocolVersion']` and `.../clientCapabilities`, and
73
+ * the `MCP-Protocol-Version`, `Mcp-Method` and (tools/call) `Mcp-Name` headers, which must
74
+ * match the body (else 400 with -32020 HeaderMismatch; missing `_meta` fields: 400, -32602).
75
+ * Methods: server/discover, tools/list, tools/call; any other gets 404 with -32601. Results
76
+ * carry `resultType: 'complete'` and `_meta['io.modelcontextprotocol/serverInfo']`; tools/list
77
+ * and server/discover are cacheable results (`ttlMs`, `cacheScope`). No subscriptions/listen
78
+ * (the tool list never changes), no MRTR input requests, no logging.
79
+ *
80
+ * The era comes from each request: a 2026-07-28 header or `_meta` version is modern, anything else
81
+ * legacy (no header: 2025-03-26, as the spec allows). An unknown version gets 400 with -32022
82
+ * (UnsupportedProtocolVersion, `data: { supported, requested }`), so a modern client retries
83
+ * with one this server speaks. The server never sends requests or notifications of its own (no
84
+ * sampling, no progress, tools/list never changes), so an SSE stream would carry nothing; without
85
+ * session ids each POST stands alone (the page state lives in the browser, not in a session).
86
+ * GET and DELETE get 405. Spec: modelcontextprotocol.io/specification/2026-07-28/basic/versioning
87
+ * and .../basic/transports/streamable-http. Hand-written like sygnal-check's stdio server (no
88
+ * dependency, D209).
89
+ *
90
+ * Tools that read the page (get_state, dispatch, component_tree, recent_actions,
91
+ * get_diagnostics, copy_as_test, agent_tools) go to an open tab through a PageBridge: in the dev
92
+ * server, Vite's HMR channel (custom events 'sygnal:mcp:*', see src/extra/devMcp.ts), request /
93
+ * response with ids and a timeout. With several tabs the most recently loaded or focused one
94
+ * answers, or the `tab` argument picks one; the result says which (`tab`), and `tabs` lists
95
+ * them with their apps. G-638: every run() app of a page is served: `app` (an index, or the
96
+ * root component's name) picks one on get_state, dispatch, component_tree, copy_as_test and
97
+ * agent_tools (default: the first, or the app that has `component`); `apps` lists them. check / graph / explain run sygnal-check's own MCP server in-process when sygnal-check
98
+ * is installed in the project.
99
+ *
100
+ * Security (the endpoint can read and change the running app): requests must come from the
101
+ * loopback interface; the Host header must be localhost, a *.localhost name, a loopback IP or a
102
+ * name in Vite's `server.allowedHosts`; a request with an Origin header must come from a
103
+ * localhost / loopback origin (a web page elsewhere, or a DNS-rebinding page, gets 403).
104
+ */
105
+ // @ts-ignore
106
+ const MCP_PATH = '/__sygnal/mcp';
107
+ const MCP_HELLO = 'sygnal:mcp:hello';
108
+ const MCP_REQUEST = 'sygnal:mcp:request';
109
+ const MCP_RESPONSE = 'sygnal:mcp:response';
110
+ /** G-655: the stateless revisions (per-request `_meta`, no initialize), newest first */
111
+ const MODERN_VERSIONS = ['2026-07-28'];
112
+ /** the `initialize` revisions, newest first; initialize answers with the client's version when it knows it */
113
+ const LEGACY_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
114
+ const SUPPORTED_VERSIONS = [...MODERN_VERSIONS, ...LEGACY_VERSIONS];
115
+ const META = 'io.modelcontextprotocol/';
116
+ const INSTRUCTIONS = 'The running Sygnal apps in the dev server\'s open page: component_tree to orient, get_state / recent_actions / get_diagnostics to read, dispatch or agent_tools to act, copy_as_test to turn a session into a test; apps lists the page\'s run() apps when there are several (`app` picks one). check, graph and explain (when present) are sygnal-check on the sources.';
117
+ /** 2026-07-28 cacheable results (tools/list, server/discover): the list is fixed while the dev server runs, not across restarts */
118
+ const CACHE = { ttlMs: 0, cacheScope: 'private' };
119
+ const MAX_BODY = 1 << 20;
120
+ class ToolError extends Error {
121
+ }
122
+ const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
123
+ const tabSchema = { type: 'integer', description: 'The tab to ask (see tabs); default: the most recently loaded or focused tab' };
124
+ const componentSchema = { type: ['string', 'integer'], description: 'A component name, or an instance id (see component_tree); default: the root' };
125
+ const appSchema = { type: ['string', 'integer'], description: "The app on the page: an index or the root component's name (see apps); default: the first app, or the one that has `component`" };
126
+ const obj = (properties, required) => ({
127
+ type: 'object', properties: { ...properties, tab: tabSchema }, ...(required ? { required } : {}), additionalProperties: false,
128
+ });
129
+ /** The tools answered by the page */
130
+ const PAGE_TOOLS = [
131
+ {
132
+ name: 'get_state',
133
+ title: 'Get component state',
134
+ description: "The live state of a component instance in the open page (the root by default). With several instances of a component (Collection items) you get each one's id and state. `path` reads one field: 'todos.0.text' or ['todos', 0, 'text'].",
135
+ inputSchema: obj({ component: componentSchema, app: appSchema, path: { type: ['string', 'array'], items: { type: ['string', 'integer'] }, description: "A field path: 'a.b.0' or ['a', 'b', 0]" } }),
136
+ annotations: { readOnlyHint: true },
137
+ },
138
+ {
139
+ name: 'dispatch',
140
+ title: 'Dispatch an action',
141
+ description: "Send an action to a component instance in the open page, as if its intent had produced it (recorded with cause 'agent'), and wait for the render. Returns the instance's new state. The action must be one of the component's model entries (an unknown name lists them).",
142
+ inputSchema: obj({ component: componentSchema, app: appSchema, action: { type: 'string', description: "The action name, e.g. 'ADD'" }, data: { description: 'The action data (any JSON value)' } }, ['action']),
143
+ },
144
+ {
145
+ name: 'component_tree',
146
+ title: 'Component tree (inspect)',
147
+ description: "The running apps' graph from inspect(): component instances (id, parentId, kind), their actions and what triggers them, state keys, context, EVENTS, children, intent selectors (matched or not) and the runtime diagnostics attached to each. `app` keeps one app's instances, `component` only that component's.",
148
+ inputSchema: obj({ component: componentSchema, app: { ...appSchema, description: "Only this app's instances: an index or the root component's name (see apps); default: every app" } }),
149
+ annotations: { readOnlyHint: true },
150
+ },
151
+ {
152
+ name: 'recent_actions',
153
+ title: 'Recent actions',
154
+ description: "The DevTools action log of the open page, newest last: type, data, component, instance, cause ('intent', 'agent', 'reply', ...), the sinks it reached and the state before / after a STATE change.",
155
+ inputSchema: obj({
156
+ limit: { type: 'integer', minimum: 1, maximum: 500, description: 'How many (default 20)' },
157
+ component: { type: 'string', description: 'Only this component' },
158
+ type: { type: 'string', description: 'Only this action type' },
159
+ cause: { type: 'string', enum: ['intent', 'next', 'reply', 'built-in', 'simulateAction', 'behavior', 'agent', 'setState'], description: 'Only this cause' },
160
+ }),
161
+ annotations: { readOnlyHint: true },
162
+ },
163
+ {
164
+ name: 'get_diagnostics',
165
+ title: 'Runtime diagnostics',
166
+ description: 'The runtime diagnostics the open page reported (SYG codes): code, severity, component, message, fix and docsUrl, with a summary. Use explain for a code you do not know.',
167
+ inputSchema: obj({
168
+ code: { type: 'string', description: "Only this code, e.g. 'SYG104'" },
169
+ severity: { type: 'string', enum: ['error', 'warn', 'info'] },
170
+ limit: { type: 'integer', minimum: 1, maximum: 500, description: 'How many, newest (default 100)' },
171
+ }),
172
+ annotations: { readOnlyHint: true },
173
+ },
174
+ {
175
+ name: 'copy_as_test',
176
+ title: 'Copy session as test',
177
+ description: "DevTools' Copy as test: a Vitest + renderComponent test file that replays what happened to a component instance in the open page (default: the root) and asserts its final state. Returns { code, complete, warnings, replayed }.",
178
+ inputSchema: obj({ component: componentSchema, app: appSchema, componentImport: { type: 'string', description: "Where the test imports the component from: a module path ('./App.jsx') or a whole import line" } }),
179
+ annotations: { readOnlyHint: true },
180
+ },
181
+ {
182
+ name: 'agent_tools',
183
+ title: "The app's agent tools",
184
+ description: "The open page's own agent tools (the `agent` statics, sygnal/ai): without `call`, the tools offered now (name, description, inputSchema, annotations) and the readable context; with `call`, runs that tool with `input` under the app's rules (validation, `when`, a consequential tool asks the person in the page first).",
185
+ inputSchema: obj({
186
+ call: { type: 'string', description: 'A tool name from the list' },
187
+ input: { description: 'The tool input' },
188
+ all: { type: 'boolean', description: 'List also the declared tools that are not offered, with why' },
189
+ app: { ...appSchema, description: "The app whose tools to use: an index or the root component's name (see apps); default: the first" },
190
+ }),
191
+ },
192
+ {
193
+ name: 'apps',
194
+ title: 'Apps on the page',
195
+ description: "The Sygnal apps (run() calls) running in the open page: index, root component, root instance id and instance count. The first is the default of the tools' `app` argument.",
196
+ inputSchema: obj({}),
197
+ annotations: { readOnlyHint: true },
198
+ },
199
+ ];
200
+ const TABS_TOOL = {
201
+ name: 'tabs',
202
+ title: 'Open pages',
203
+ description: 'The pages of this dev server connected to the endpoint (id, url, title, apps: { index, component } per run() app); the page tools use the most recently loaded or focused one unless given `tab`.',
204
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
205
+ annotations: { readOnlyHint: true },
206
+ };
207
+ /** sygnal-check's MCP server (its `src/mcp.js`), loaded from the project; undefined when missing */
208
+ async function loadCheckServer(root) {
209
+ try {
210
+ const main = node_module.createRequire(path.join(root, 'package.json')).resolve('sygnal-check');
211
+ const file = path.join(path.dirname(main), 'mcp.js');
212
+ if (!fs.existsSync(file))
213
+ return undefined;
214
+ const mod = await import(/* @vite-ignore */ node_url.pathToFileURL(file).href);
215
+ if (typeof mod.createMcpServer !== 'function' || !Array.isArray(mod.TOOLS))
216
+ return undefined;
217
+ return { tools: mod.TOOLS, server: mod.createMcpServer({ cwd: root }) };
218
+ }
219
+ catch (_) {
220
+ return undefined;
221
+ }
222
+ }
223
+ /** The JSON-RPC side of the endpoint (transport-free; tests drive it directly) */
224
+ function createDevMcpServer(o) {
225
+ const timeout = o.timeout && o.timeout > 0 ? o.timeout : 10000;
226
+ const result = (id, value) => ({ jsonrpc: '2.0', id, result: value });
227
+ const error = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
228
+ let check;
229
+ const getCheck = async () => (check === undefined ? (check = (await o.check) || null) : check);
230
+ const pickTab = (want) => {
231
+ const tabs = o.bridge.tabs();
232
+ if (!tabs.length) {
233
+ const url = o.url?.();
234
+ throw new ToolError(`no page is connected: open the app${url ? ` (${url})` : ''} in a browser, with the dev server running`);
235
+ }
236
+ if (want !== undefined && want !== null) {
237
+ const tab = tabs.find(t => t.id === Number(want));
238
+ if (!tab)
239
+ throw new ToolError(`no tab ${want}; open tabs: ${tabs.map(t => `${t.id} ${t.url}`).join(', ')}`);
240
+ return { tab };
241
+ }
242
+ const tab = tabs.reduce((a, b) => (b.active > a.active ? b : a));
243
+ return { tab, note: tabs.length > 1 ? `${tabs.length} tabs are open; used tab ${tab.id} (the most recently loaded or focused). Pass tab to choose one (see tabs).` : undefined };
244
+ };
245
+ const content = (value, isError = false) => {
246
+ const structured = isObj(value) ? value : { value };
247
+ return { content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }], structuredContent: structured, isError };
248
+ };
249
+ async function callTool(name, args) {
250
+ if (name === 'tabs')
251
+ return content({ tabs: o.bridge.tabs().map(({ id, url, title, apps }) => ({ id, url, title, ...(apps ? { apps } : {}) })) });
252
+ const { tab: want, ...rest } = args;
253
+ const { tab, note } = pickTab(want);
254
+ const value = await o.bridge.request(tab.id, name, rest, timeout);
255
+ const out = isObj(value) ? value : { value };
256
+ return content({ ...out, tab: { id: tab.id, url: tab.url, title: tab.title }, ...(note ? { tabNote: note } : {}) });
257
+ }
258
+ const serverInfo = { name: 'sygnal-dev', title: 'Sygnal dev server', version: o.version || '0.0.0' };
259
+ async function handleMessage(msg, modern) {
260
+ if (!isObj(msg) || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') {
261
+ return isObj(msg) && 'id' in msg && !('result' in msg || 'error' in msg) ? error(msg.id ?? null, -32600, 'Invalid Request') : null;
262
+ }
263
+ if (!('id' in msg) || msg.id === null)
264
+ return null; // a notification
265
+ const { id, method } = msg;
266
+ const params = isObj(msg.params) ? msg.params : {};
267
+ // G-655: 2026-07-28 removed initialize and ping, and added server/discover
268
+ if (modern ? method === 'initialize' || method === 'ping' : method === 'server/discover')
269
+ return error(id, -32601, `Method not found: ${method}`);
270
+ switch (method) {
271
+ case 'initialize': {
272
+ const requested = params.protocolVersion;
273
+ return result(id, {
274
+ protocolVersion: LEGACY_VERSIONS.includes(requested) ? requested : LEGACY_VERSIONS[0],
275
+ capabilities: { tools: { listChanged: false } },
276
+ serverInfo,
277
+ instructions: INSTRUCTIONS,
278
+ });
279
+ }
280
+ case 'server/discover':
281
+ return result(id, { supportedVersions: SUPPORTED_VERSIONS, capabilities: { tools: {} }, instructions: INSTRUCTIONS, ...CACHE });
282
+ case 'ping':
283
+ return result(id, {});
284
+ case 'tools/list': {
285
+ const c = await getCheck();
286
+ return result(id, { tools: [...PAGE_TOOLS, TABS_TOOL, ...(c ? c.tools : [])], ...(modern ? CACHE : {}) });
287
+ }
288
+ case 'tools/call': {
289
+ const name = params.name;
290
+ const args = isObj(params.arguments) ? params.arguments : {};
291
+ if (name === 'tabs' || PAGE_TOOLS.some(t => t.name === name)) {
292
+ try {
293
+ return result(id, await callTool(name, args));
294
+ }
295
+ catch (err) {
296
+ return result(id, { content: [{ type: 'text', text: err instanceof ToolError ? err.message : `${name} failed: ${err?.message || err}` }], isError: true });
297
+ }
298
+ }
299
+ const c = await getCheck();
300
+ if (c && c.tools.some((t) => t.name === name)) {
301
+ // without `_meta`, sygnal-check's server answers in the initialize era's shape (G-656: it
302
+ // speaks 2026-07-28 too, but this endpoint checked the envelope and adds the fields itself)
303
+ const { _meta, ...rest } = params;
304
+ return { ...c.server.handle({ ...msg, params: { ...rest, arguments: args } }), id };
305
+ }
306
+ return error(id, -32602, `Unknown tool: ${name}`);
307
+ }
308
+ default:
309
+ return error(id, -32601, `Method not found: ${method}`);
310
+ }
311
+ }
312
+ /**
313
+ * One message (or a legacy batch); never throws. `modern`: a 2026-07-28 request whose envelope
314
+ * (version, headers, `_meta` fields) the transport checked (see mcpMiddleware); its result is
315
+ * marked `resultType: 'complete'` and names the server in `_meta`.
316
+ */
317
+ async function handle(msg, modern = false) {
318
+ if (Array.isArray(msg)) {
319
+ if (modern)
320
+ return error(null, -32600, 'Invalid Request: one JSON-RPC message per POST');
321
+ const out = (await Promise.all(msg.map((m) => handle(m)))).filter(Boolean);
322
+ return out.length ? out : null;
323
+ }
324
+ try {
325
+ const out = await handleMessage(msg, modern);
326
+ if (modern && out && isObj(out.result)) {
327
+ const meta = isObj(out.result._meta) ? out.result._meta : {};
328
+ out.result = { resultType: 'complete', ...out.result, _meta: { ...meta, [META + 'serverInfo']: serverInfo } };
329
+ }
330
+ return out;
331
+ }
332
+ catch (err) {
333
+ return error(isObj(msg) && 'id' in msg ? msg.id ?? null : null, -32603, `Internal error: ${err?.message || err}`);
334
+ }
335
+ }
336
+ return { handle };
337
+ }
338
+ // ---------------------------------------------------------------- security
339
+ const LOOPBACK_IP = /^(127\.\d{1,3}\.\d{1,3}\.\d{1,3}|::1|::ffff:127\.\d{1,3}\.\d{1,3}\.\d{1,3})$/;
340
+ /** The host part of a Host header value or URL host ('[::1]:5173' → '::1') */
341
+ function hostname(host) {
342
+ const h = host.trim().toLowerCase();
343
+ if (h.startsWith('['))
344
+ return h.slice(1, h.indexOf(']') > 0 ? h.indexOf(']') : undefined);
345
+ const i = h.lastIndexOf(':');
346
+ return i > 0 && h.indexOf(':') === i ? h.slice(0, i) : h;
347
+ }
348
+ const isLocalName = (h) => h === 'localhost' || h.endsWith('.localhost') || LOOPBACK_IP.test(h);
349
+ /** Vite's allowedHosts entries: 'example.test', or '.example.test' for it and its subdomains */
350
+ function inAllowed(h, allowed) {
351
+ if (!Array.isArray(allowed))
352
+ return false;
353
+ return allowed.some((a) => {
354
+ if (typeof a !== 'string' || !a)
355
+ return false;
356
+ const e = a.toLowerCase();
357
+ return e.startsWith('.') ? h === e.slice(1) || h.endsWith(e) : h === e;
358
+ });
359
+ }
360
+ /** Why the request is refused (403), or null */
361
+ function refusal(req, allowedHosts) {
362
+ const remote = String(req?.socket?.remoteAddress || '');
363
+ if (!LOOPBACK_IP.test(remote))
364
+ return `the Sygnal MCP endpoint only answers requests from this machine (not ${remote || 'an unknown address'})`;
365
+ const host = req?.headers?.host;
366
+ if (typeof host !== 'string' || !host)
367
+ return 'missing Host header';
368
+ const h = hostname(host);
369
+ if (!isLocalName(h) && !inAllowed(h, allowedHosts))
370
+ return `Host ${host} is not a local name (add it to server.allowedHosts to allow it)`;
371
+ const origin = req?.headers?.origin;
372
+ if (origin !== undefined && origin !== 'null') {
373
+ let oh = '';
374
+ try {
375
+ oh = new URL(String(origin)).hostname.replace(/^\[|\]$/g, '').toLowerCase();
376
+ }
377
+ catch (_) { }
378
+ if (!oh || (!isLocalName(oh) && !inAllowed(oh, allowedHosts)))
379
+ return `Origin ${origin} is not allowed`;
380
+ }
381
+ else if (origin === 'null') {
382
+ return 'Origin null is not allowed';
383
+ }
384
+ return null;
385
+ }
386
+ // ---------------------------------------------------------------- HTTP
387
+ function send(res, status, body, headers = {}) {
388
+ res.statusCode = status;
389
+ for (const k in headers)
390
+ res.setHeader(k, headers[k]);
391
+ if (body === undefined)
392
+ return res.end();
393
+ res.setHeader('Content-Type', 'application/json');
394
+ res.end(JSON.stringify(body));
395
+ }
396
+ function readBody(req) {
397
+ return new Promise((resolve, reject) => {
398
+ let size = 0;
399
+ const chunks = [];
400
+ req.on('data', (c) => {
401
+ size += c.length;
402
+ if (size > MAX_BODY) {
403
+ reject(new Error('too large'));
404
+ req.destroy?.();
405
+ }
406
+ else
407
+ chunks.push(c);
408
+ });
409
+ req.on('end', () => resolve(globalThis.Buffer.concat(chunks).toString('utf8')));
410
+ req.on('error', reject);
411
+ });
412
+ }
413
+ /** An `Mcp-Name` value: plain, or the spec's `=?base64?…?=` sentinel for one that isn't header-safe */
414
+ function headerValue(v) {
415
+ const m = /^=\?base64\?(.*)\?=$/.exec(v);
416
+ return m ? globalThis.Buffer.from(m[1], 'base64').toString('utf8') : v;
417
+ }
418
+ /**
419
+ * G-655: why a 2026-07-28 request's envelope is refused, as [code, message], or null. The spec
420
+ * (basic/index "_meta", transports/streamable-http "Server Validation"): `_meta` must carry the
421
+ * protocol version and the client capabilities (-32602), and the MCP-Protocol-Version, Mcp-Method
422
+ * and (tools/call, resources/read, prompts/get) Mcp-Name headers must be there and match the body
423
+ * (-32020 HeaderMismatch). This server marks no parameter with `x-mcp-header`, so no
424
+ * Mcp-Param-* header is required.
425
+ */
426
+ function modernEnvelope(msg, headers) {
427
+ const params = isObj(msg.params) ? msg.params : {};
428
+ const meta = isObj(params._meta) ? params._meta : {};
429
+ if (typeof meta[META + 'protocolVersion'] !== 'string')
430
+ return [-32602, `Invalid params: _meta["${META}protocolVersion"] is required`];
431
+ if (!isObj(meta[META + 'clientCapabilities']))
432
+ return [-32602, `Invalid params: _meta["${META}clientCapabilities"] is required`];
433
+ const header = headers['mcp-protocol-version'];
434
+ if (header !== meta[META + 'protocolVersion']) {
435
+ return [-32020, header === undefined ? 'Header mismatch: MCP-Protocol-Version header is required' : `Header mismatch: MCP-Protocol-Version header value '${header}' does not match body value '${meta[META + 'protocolVersion']}'`];
436
+ }
437
+ const method = headers['mcp-method'];
438
+ if (method !== msg.method)
439
+ return [-32020, method === undefined ? 'Header mismatch: Mcp-Method header is required' : `Header mismatch: Mcp-Method header value '${method}' does not match body value '${msg.method}'`];
440
+ if (msg.method === 'tools/call' || msg.method === 'prompts/get' || msg.method === 'resources/read') {
441
+ const want = msg.method === 'resources/read' ? params.uri : params.name;
442
+ const name = headers['mcp-name'];
443
+ if (typeof name !== 'string')
444
+ return [-32020, 'Header mismatch: Mcp-Name header is required'];
445
+ if (headerValue(name) !== want)
446
+ return [-32020, `Header mismatch: Mcp-Name header value '${name}' does not match body value '${want}'`];
447
+ }
448
+ return null;
449
+ }
450
+ /** The connect middleware for MCP_PATH */
451
+ function mcpMiddleware(server, allowedHosts) {
452
+ return async (req, res, next) => {
453
+ const url = String(req.url || '').replace(/[?#].*$/, '');
454
+ if (url !== MCP_PATH && url !== MCP_PATH + '/')
455
+ return next();
456
+ const why = refusal(req, allowedHosts());
457
+ if (why)
458
+ return send(res, 403, { jsonrpc: '2.0', id: null, error: { code: -32000, message: `Forbidden: ${why}` } });
459
+ // G-638: no SSE stream on GET (the spec's "MUST ... return 405" for a server that offers none), no sessions to DELETE
460
+ if (req.method !== 'POST')
461
+ return send(res, 405, { jsonrpc: '2.0', id: null, error: { code: -32000, message: 'Method not allowed: this endpoint takes JSON-RPC over POST and answers with application/json (no SSE stream, no sessions)' } }, { Allow: 'POST' });
462
+ let msg;
463
+ try {
464
+ msg = JSON.parse(await readBody(req));
465
+ }
466
+ catch (err) {
467
+ if (err?.message === 'too large')
468
+ return send(res, 413, { jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Request too large' } });
469
+ return send(res, 400, { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
470
+ }
471
+ // G-655: the era is per request: a 2026-07-28 version in the header or in `_meta` is modern
472
+ // (stateless), anything else the initialize era (no header: 2025-03-26)
473
+ const id = isObj(msg) && 'id' in msg ? msg.id ?? null : null;
474
+ const fail = (status, code, message, data) => send(res, status, { jsonrpc: '2.0', id, error: { code, message, ...(data ? { data } : {}) } });
475
+ const header = req.headers['mcp-protocol-version'];
476
+ const meta = isObj(msg) && isObj(msg.params) && isObj(msg.params._meta) ? msg.params._meta : {};
477
+ const metaVersion = meta[META + 'protocolVersion'];
478
+ for (const requested of [header, metaVersion]) {
479
+ if (requested !== undefined && !SUPPORTED_VERSIONS.includes(requested)) {
480
+ return fail(400, -32022, 'Unsupported protocol version', { supported: SUPPORTED_VERSIONS, requested });
481
+ }
482
+ }
483
+ const modern = MODERN_VERSIONS.includes(header) || MODERN_VERSIONS.includes(metaVersion);
484
+ if (modern) {
485
+ if (!isObj(msg))
486
+ return fail(400, -32600, 'Invalid Request: one JSON-RPC message per POST');
487
+ if ('id' in msg && msg.id !== null) {
488
+ const why = modernEnvelope(msg, req.headers);
489
+ if (why)
490
+ return fail(400, why[0], why[1]);
491
+ }
492
+ }
493
+ const out = await server.handle(msg, modern);
494
+ if (out === null || out === undefined)
495
+ return send(res, 202);
496
+ // 2026-07-28: a method this server doesn't implement is 404 (the JSON-RPC body tells it from a missing endpoint)
497
+ send(res, modern && out.error?.code === -32601 ? 404 : 200, out);
498
+ };
499
+ }
500
+ // ---------------------------------------------------------------- the page bridge (Vite's HMR channel)
501
+ /** A PageBridge over the dev server's HMR channel (server.ws / environments.client.hot) */
502
+ function hmrBridge(server) {
503
+ const ws = server?.ws || server?.environments?.client?.hot;
504
+ const tabs = new Map();
505
+ const pending = new Map();
506
+ let nextTab = 1;
507
+ let nextReq = 1;
508
+ let clock = 0;
509
+ const fail = (client) => {
510
+ const t = tabs.get(client);
511
+ tabs.delete(client);
512
+ if (!t)
513
+ return;
514
+ for (const [id, p] of pending) {
515
+ if (p.client !== client)
516
+ continue;
517
+ clearTimeout(p.timer);
518
+ pending.delete(id);
519
+ p.reject(new ToolError(`tab ${t.id} closed before it answered`));
520
+ }
521
+ };
522
+ if (ws && typeof ws.on === 'function') {
523
+ ws.on(MCP_HELLO, (data, client) => {
524
+ if (!client)
525
+ return;
526
+ let t = tabs.get(client);
527
+ if (!t) {
528
+ tabs.set(client, t = { id: nextTab++, url: '', title: '', active: 0 });
529
+ // Vite's socket client carries its WebSocket: a closed tab leaves at once
530
+ try {
531
+ client.socket?.once?.('close', () => fail(client));
532
+ }
533
+ catch (_) { }
534
+ }
535
+ t.url = String(data?.url || '');
536
+ t.title = String(data?.title || '');
537
+ if (Array.isArray(data?.apps))
538
+ t.apps = data.apps.map((a) => ({ index: Number(a?.index), component: String(a?.component ?? '') }));
539
+ // an apps update (an HMR swap, a second run()) is not a focus
540
+ if (!data?.update || !t.active)
541
+ t.active = ++clock;
542
+ });
543
+ ws.on(MCP_RESPONSE, (data, client) => {
544
+ const p = data && pending.get(data.id);
545
+ if (!p || p.client !== client)
546
+ return;
547
+ if (data.waiting) {
548
+ clearTimeout(p.timer);
549
+ p.timer = setTimeout(() => expire(data.id), 5 * 60000);
550
+ return;
551
+ }
552
+ clearTimeout(p.timer);
553
+ pending.delete(data.id);
554
+ if (data.ok)
555
+ p.resolve(data.result);
556
+ else
557
+ p.reject(new ToolError(String(data.error || 'the page reported an error')));
558
+ });
559
+ }
560
+ /** tabs whose socket is still open (when the channel tells) */
561
+ const live = () => {
562
+ let open;
563
+ try {
564
+ open = ws?.clients;
565
+ }
566
+ catch (_) { }
567
+ for (const c of [...tabs.keys()]) {
568
+ const closed = c.socket && typeof c.socket.readyState === 'number' && c.socket.readyState > 1;
569
+ if (closed || (open instanceof Set && c.socket && !open.has(c)))
570
+ fail(c);
571
+ }
572
+ return tabs;
573
+ };
574
+ function expire(id) {
575
+ const p = pending.get(id);
576
+ if (!p)
577
+ return;
578
+ pending.delete(id);
579
+ p.reject(new ToolError(`tab ${p.tab} did not answer in time (is the page frozen, or a dialog open?)`));
580
+ }
581
+ return {
582
+ tabs: () => [...live().values()].map(t => ({ ...t, ...(t.apps ? { apps: t.apps.map(a => ({ ...a })) } : {}) })),
583
+ request(tabId, tool, args, ms) {
584
+ const client = [...live().entries()].find(([, t]) => t.id === tabId)?.[0];
585
+ if (!client)
586
+ return Promise.reject(new ToolError(`no tab ${tabId}`));
587
+ const id = nextReq++;
588
+ return new Promise((resolve, reject) => {
589
+ const timer = setTimeout(() => expire(id), ms);
590
+ pending.set(id, { tab: tabId, resolve, reject, timer, client });
591
+ try {
592
+ client.send({ type: 'custom', event: MCP_REQUEST, data: { id, tool, args } });
593
+ }
594
+ catch (err) {
595
+ clearTimeout(timer);
596
+ pending.delete(id);
597
+ fail(client);
598
+ reject(new ToolError(`tab ${tabId} is gone: ${err?.message || err}`));
599
+ }
600
+ });
601
+ },
602
+ };
603
+ }
604
+ /** Wire the endpoint into a Vite dev server */
605
+ function attachMcp(server, root, options, version) {
606
+ const bridge = hmrBridge(server);
607
+ const mcp = createDevMcpServer({
608
+ ...options,
609
+ bridge,
610
+ version,
611
+ check: loadCheckServer(root),
612
+ url: () => server?.resolvedUrls?.local?.[0],
613
+ });
614
+ server.middlewares.use(mcpMiddleware(mcp, () => server?.config?.server?.allowedHosts));
615
+ return mcp;
616
+ }
617
+ /** 'virtual:sygnal/mcp': connects the page (dev only) */
618
+ function mcpClientModule(options) {
619
+ const confirm = options.confirm === undefined ? 'page' : options.confirm;
620
+ return `import { agentTools } from 'sygnal';
621
+ import { installMcpBridge } from 'sygnal/devtools';
622
+ if (import.meta.hot) installMcpBridge(import.meta.hot, { agentTools, confirm: ${JSON.stringify(confirm)} });
623
+ `;
624
+ }
625
+
8
626
  /**
9
627
  * Sygnal Vite Plugin
10
628
  *
@@ -19,7 +637,10 @@ var node_url = require('node:url');
19
637
  * What it does:
20
638
  * 1. Configures the automatic JSX transform with sygnal as the import source:
21
639
  * `oxc` (Vite 8), plus `esbuild` under Vite 7 and older
22
- * 2. Detects files that call `run()` from sygnal and auto-injects HMR wiring
640
+ * 2. Detects files that call `run()` from sygnal and auto-injects HMR wiring.
641
+ * A module that calls `defineElement()` from 'sygnal/element' at the top level
642
+ * (and doesn't import run()) gets `if (import.meta.hot) import.meta.hot.accept()`
643
+ * appended: re-running it swaps the elements' component, keeping their state
23
644
  * 3. Dev mode (`vite` / `vite dev` only; never `vite build`). Into every
24
645
  * file that imports `run` from sygnal (including entries with manual HMR
25
646
  * wiring or `disableHmr`) it inserts, on an existing line:
@@ -55,9 +676,14 @@ var node_url = require('node:url');
55
676
  * current findings when the page loads ('sygnal:check:request'), and only
56
677
  * that client is answered; after a source change the new findings go to
57
678
  * 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
679
+ * findings also open Vite's error overlay (`overlay: false`: never):
680
+ * SYG112, SYG124, SYG125, SYG128, SYG405 and the checker's other
681
+ * error-severity codes. The a11y lane SYG701-708 is a warning, also
682
+ * under `diagnostics.strict` (D144, amending D111); `check.a11y: 'error'`
683
+ * makes it an error (and opens the overlay). Without that opt-in a 7xx
684
+ * finding never opens the overlay, even from an older sygnal-check that
685
+ * reports it as an error under strict. Everything else is a warning or
686
+ * info and goes to the console. Warnings never use the overlay: while
61
687
  * one is open Vite's client reloads the page on the next HMR update
62
688
  * (`overlay: 'warn'` is treated as 'error', with a notice). The dev
63
689
  * client closes a sygnal-check overlay before each update, and the
@@ -78,13 +704,35 @@ var node_url = require('node:url');
78
704
  * 7. Vike and Astro (G-014): the app is started by sygnal's own client
79
705
  * entry, which user code doesn't import. Client-side imports of
80
706
  * 'sygnal/vike/onRenderClient' resolve to a dev wrapper that sets the
81
- * flag and loads the checks first. Astro resolves its renderer entry on
707
+ * flag and loads the checks first. That entry is excluded from
708
+ * pre-bundling in dev (B-020), so it shares the pages' core. Astro resolves its renderer entry on
82
709
  * the server and the browser loads the file itself, so the same snippet
83
710
  * is added to sygnal's 'astro/client' file by the transform. That file
84
711
  * imports the public 'sygnal' entry (one shared core, B-019), so its
85
712
  * 'sygnal' import gets the run() wrapper too: every `diagnostics` mode
86
713
  * and ignore list works for islands. The sygnal/astro integration adds
87
714
  * this plugin in `astro dev`.
715
+ * 8. `nativeGlobalThis` (G-099, default true): a `resolve.alias` from
716
+ * `globalthis` (xstream's `require('globalthis').getPolyfill()`) to the
717
+ * package's dist/shims/globalthis.cjs, which returns the native
718
+ * globalThis. Drops the polyfill chain (~4 KB gzip) from every bundle,
719
+ * in dev (including pre-bundling), build and Vitest. Dependencies left
720
+ * external (SSR, Vitest) still load the real package from Node. The
721
+ * sygnal/astro integration adds the same alias in `astro build`. Any
722
+ * dependency's `require('globalthis')` gets the stub; a `globalthis`
723
+ * entry in the user's own `resolve.alias` wins (R2-7).
724
+ * 9. `devtools` option (D77, default true): in dev the same files (run()
725
+ * importers, the Vike wrapper, the Astro client) also get
726
+ * `import 'sygnal/devtools';`, first, which installs the DevTools bridge
727
+ * (window.__SYGNAL_DEVTOOLS__) for the browser extension. Independent of
728
+ * `diagnostics` ('off' still injects it). Never in `vite build` or under
729
+ * Vitest: production builds carry no DevTools code. `devtools: false`
730
+ * injects nothing. PLAN-4 3-E: `devtools: { redux: true }` also calls
731
+ * connectReduxDevtools() (actions + state to the Redux DevTools extension).
732
+ * 10. `mcp` option (PLAN-6 E-1, default false): the dev server serves an MCP endpoint at
733
+ * /__sygnal/mcp (./mcp.ts: streamable HTTP, loopback / local Host / local Origin only)
734
+ * and the dev entries also import 'virtual:sygnal/mcp', which connects the page over the
735
+ * HMR channel (installMcpBridge from 'sygnal/devtools'). Never in `vite build` or Vitest.
88
736
  *
89
737
  * Why not Vite's `define`? Vite's dependency optimizer does not apply user
90
738
  * `define` replacements to pre-bundled dependencies (only process.env.NODE_ENV),
@@ -125,11 +773,14 @@ const CHECK_EVENT = 'sygnal:check';
125
773
  // Sent by the dev client when it loads: the server answers that client only
126
774
  const CHECK_REQUEST = 'sygnal:check:request';
127
775
  const OVERLAY_PLUGIN = 'sygnal-check';
776
+ // PLAN-6 E-1: the page side of the dev MCP endpoint
777
+ const MCP_CLIENT = 'virtual:sygnal/mcp';
778
+ const MCP_CLIENT_ID = '\0' + MCP_CLIENT;
128
779
  // Register the runtime checks with the core that loaded last (see the Vike wrapper)
129
780
  const REINSTALL_IMPORT = `import { installChecks as __sygnalInstallChecks } from 'sygnal/diagnostics';`;
130
781
  const REINSTALL_CALL = 'try { __sygnalInstallChecks() } catch (e) { console.warn(e) }\n';
131
782
  function sygnal(options = {}) {
132
- const { disableJsx = false, disableHmr = false, vitestSetup = true } = options;
783
+ const { disableJsx = false, disableHmr = false, vitestSetup = true, nativeGlobalThis = true } = options;
133
784
  const diagnostics = normalizeDiagnostics(options.diagnostics);
134
785
  const devOn = diagnostics.mode !== 'off';
135
786
  // A mode other than the flag's 'warn', or an ignore list, needs run()'s option
@@ -142,6 +793,18 @@ function sygnal(options = {}) {
142
793
  // Statements run before the app starts: dev flag (+ strict flag)
143
794
  const flags = DEV_FLAG + (diagnostics.strict ? STRICT_FLAG : '');
144
795
  const devImports = `import 'sygnal/diagnostics';import '${DEV_CLIENT}';`;
796
+ // D77: the DevTools bridge, dev only. First, so the diagnostics entry finds it.
797
+ // E-1: the MCP endpoint needs the bridge, so `mcp` turns it on
798
+ const mcpOn = !!options.mcp;
799
+ const mcpOptions = typeof options.mcp === 'object' && options.mcp ? options.mcp : {};
800
+ const devtoolsOn = options.devtools !== false || mcpOn;
801
+ const redux = typeof options.devtools == 'object' && !!options.devtools?.redux;
802
+ const devtoolsImport = (!devtoolsOn ? ''
803
+ : redux ? `import { connectReduxDevtools as __sygnalReduxDevtools } from 'sygnal/devtools';__sygnalReduxDevtools();`
804
+ : `import 'sygnal/devtools';`) + (mcpOn ? `import '${MCP_CLIENT}';` : '');
805
+ // What a dev entry gets: DevTools, then (unless diagnostics are 'off') flags + checks
806
+ const devSnippet = devtoolsImport + (devOn ? flags + devImports : '');
807
+ const devInject = devOn || devtoolsOn;
145
808
  let astroClient;
146
809
  return {
147
810
  name: 'vite-plugin-sygnal',
@@ -157,6 +820,12 @@ function sygnal(options = {}) {
157
820
  noExternal: ['sygnal'],
158
821
  },
159
822
  };
823
+ // G-099: xstream's `require('globalthis')` gets the native-globalThis
824
+ // stub. An alias (not resolveId) so it also applies to pre-bundling.
825
+ // R2-7: not when the user's config already aliases `globalthis`.
826
+ const alias = nativeGlobalThis ? globalThisAlias(config?.resolve?.alias) : [];
827
+ if (alias.length)
828
+ result.resolve = { alias };
160
829
  if (!disableJsx) {
161
830
  result.oxc = {
162
831
  jsx: {
@@ -188,13 +857,31 @@ function sygnal(options = {}) {
188
857
  result.test = { setupFiles: [setup] };
189
858
  }
190
859
  }
191
- else if (isServe && devOn && sygnalInNodeModules(root)) {
860
+ else if (isServe && devInject && sygnalInNodeModules(root)) {
192
861
  // 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'] };
862
+ // the checks (or DevTools) entry: pre-bundle it with sygnal so the first
863
+ // page load doesn't trigger a re-optimization. Only for an installed
864
+ // sygnal: a linked one is served from source, and pre-bundling the
865
+ // checks alone would give them their own copy of the core.
866
+ const include = [...(devtoolsOn ? ['sygnal/devtools'] : []), ...(devOn ? ['sygnal/diagnostics'] : [])];
867
+ result.optimizeDeps = { ...result.optimizeDeps, include };
868
+ }
869
+ // B-020: Vike adds its client entry 'sygnal/vike/onRenderClient' to
870
+ // optimizeDeps.include. With a linked sygnal, the pages' 'sygnal' is
871
+ // served from source while that pre-bundle inlines a second copy of the
872
+ // core. Unbundled, the entry's own `import 'sygnal'` resolves like the
873
+ // pages' imports (source when linked, the shared pre-bundle when
874
+ // installed): one core. Vike honours the exclude list.
875
+ // G-098: Vite doesn't discover the imports of an excluded dependency, so for an
876
+ // installed sygnal 'sygnal' is pre-bundled up front (pages that reach it only through
877
+ // the JSX runtime would otherwise get it, and its CommonJS xstream, unbundled).
878
+ if (isServe && !isVitest) {
879
+ const include = result.optimizeDeps?.include || [];
880
+ result.optimizeDeps = {
881
+ ...result.optimizeDeps,
882
+ exclude: [VIKE_CLIENT],
883
+ ...(sygnalInNodeModules(root) ? { include: [...include, 'sygnal'] } : {}),
884
+ };
198
885
  }
199
886
  return result;
200
887
  },
@@ -227,10 +914,12 @@ function sygnal(options = {}) {
227
914
  return null;
228
915
  if (source === DEV_CLIENT)
229
916
  return DEV_CLIENT_ID;
917
+ if (source === MCP_CLIENT && mcpOn)
918
+ return MCP_CLIENT_ID;
230
919
  if (source === 'sygnal' && importer && wrapRun && runtimeImporters.has(cleanId(importer))) {
231
920
  return RUNTIME_ID;
232
921
  }
233
- if (source === VIKE_CLIENT && devOn && !opts?.ssr) {
922
+ if (source === VIKE_CLIENT && devInject && !opts?.ssr) {
234
923
  if (importer !== VIKE_CLIENT_ID)
235
924
  return VIKE_CLIENT_ID;
236
925
  // The wrapper's own import of the real entry: resolve it normally.
@@ -247,18 +936,28 @@ function sygnal(options = {}) {
247
936
  load(id) {
248
937
  if (id === DEV_CLIENT_ID)
249
938
  return devClientModule();
939
+ if (id === MCP_CLIENT_ID && mcpOn)
940
+ return mcpClientModule(mcpOptions);
250
941
  if (id === RUNTIME_ID)
251
942
  return runtimeModule(diagnostics);
252
943
  // The checks register again once the real entry has loaded: a
253
944
  // pre-bundled copy of it can carry its own copy of the Sygnal core
254
945
  // (e.g. with a linked sygnal), which then holds the diagnostics bridge.
255
946
  if (id === VIKE_CLIENT_ID) {
256
- return `${devImports}${REINSTALL_IMPORT}\nexport * from '${VIKE_CLIENT}';\n${flags}\n${REINSTALL_CALL}`;
947
+ if (!devOn)
948
+ return `${devtoolsImport}\nexport * from '${VIKE_CLIENT}';\n`;
949
+ return `${devtoolsImport}${devImports}${REINSTALL_IMPORT}\nexport * from '${VIKE_CLIENT}';\n${flags}\n${REINSTALL_CALL}`;
257
950
  }
258
951
  return null;
259
952
  },
260
953
  configureServer(server) {
261
- if (isVitest || options.check === false)
954
+ if (isVitest)
955
+ return;
956
+ // E-1: the MCP endpoint (a middleware ahead of Vite's own; it does its own host checks)
957
+ if (mcpOn && server?.middlewares) {
958
+ attachMcp(server, root, mcpOptions, sygnalVersion(root));
959
+ }
960
+ if (options.check === false)
262
961
  return;
263
962
  const checkOptions = typeof options.check === 'object' && options.check ? options.check : {};
264
963
  // Deferred: the dev server doesn't wait for the first check
@@ -269,84 +968,176 @@ function sygnal(options = {}) {
269
968
  }, 0);
270
969
  },
271
970
  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));
971
+ // SYG666 (PLAN-5 W-2): an adapter entry imported without its optional peers; every mode
972
+ const adapters = adapterImports(this, code, id);
973
+ if (adapters)
974
+ return checkPeers(this, adapters, id).then(() => devTransform.call(this, code, id, opts));
975
+ return devTransform.call(this, code, id, opts);
976
+ },
977
+ };
978
+ function devTransform(code, id, opts) {
979
+ if (!isServe)
980
+ return null;
981
+ // sygnal/astro/client (Astro islands): flags + checks first. It imports
982
+ // the public 'sygnal' entry (the app's core, B-019), which resolves to
983
+ // the run() wrapper when the diagnostics option needs it.
984
+ if (devInject && !isVitest && !opts?.ssr) {
985
+ astroClient = astroClient || astroClientFiles(root);
986
+ if (astroClient.has(cleanId(id))) {
310
987
  if (wrapRun)
311
988
  runtimeImporters.add(cleanId(id));
989
+ return withSourcemap(code, [[0, devSnippet]], '', id);
312
990
  }
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`));
991
+ }
992
+ // Only transform JS/TS/JSX/TSX files
993
+ if (!/\.[jt]sx?$/.test(id))
994
+ return null;
995
+ // Skip node_modules
996
+ if (id.includes('node_modules'))
997
+ return null;
998
+ // Must import run from sygnal
999
+ if (!code.includes('sygnal'))
1000
+ return null;
1001
+ // Match against the code with comments blanked out (same offsets), so
1002
+ // e.g. `// run(App)` or a commented-out import is never matched.
1003
+ const noComments = blankOut(code, false);
1004
+ // Find: import { run, ... } from 'sygnal'
1005
+ const runImportRe = /import\s+\{[^}]*\brun\b[^}]*\}\s+from\s+['"]sygnal['"]/;
1006
+ if (!runImportRe.test(noComments)) {
1007
+ return disableHmr || isVitest ? null : elementHmr(code, noComments, id);
1008
+ }
1009
+ // Dev snippet: injected into every file that imports run() — also when
1010
+ // HMR wiring below is skipped. Inserted after any shebang line and
1011
+ // directive prologue, without adding a line. Nothing under Vitest: a
1012
+ // global flag set by one test file would leak dev/strict mode into the
1013
+ // later test files of the same worker; there the checks come from
1014
+ // test.setupFiles and renderComponent() manages the modes.
1015
+ const inserts = [];
1016
+ if (devInject && !isVitest) {
1017
+ inserts.push(flagInsertion(code, devSnippet));
1018
+ if (wrapRun)
1019
+ runtimeImporters.add(cleanId(id));
1020
+ }
1021
+ const done = (tail = '') => (inserts.length || tail ? withSourcemap(code, inserts, tail, id) : null);
1022
+ // Skip HMR wiring if disabled, in tests, or already manually wired
1023
+ if (disableHmr || isVitest || /\.(test|spec)\.[jt]sx?$/.test(id))
1024
+ return done();
1025
+ if (noComments.includes('import.meta.hot'))
1026
+ return done();
1027
+ // Strings and template literals blanked out too: a statement-level match only
1028
+ const runCall = RUN_CALL_RE.exec(blankOut(code, true));
1029
+ if (!runCall)
1030
+ return done();
1031
+ const [, binding, componentName] = runCall;
1032
+ // Find the import path for this component
1033
+ // Handles: import App from './App.jsx'
1034
+ // import App from "./App"
1035
+ const componentImportRe = new RegExp(`import\\s+${componentName.replace(/\$/g, '\\$')}\\s+from\\s+['"]([^'"]+)['"]`);
1036
+ const componentImportMatch = noComments.match(componentImportRe);
1037
+ if (!componentImportMatch)
1038
+ return done();
1039
+ const componentPath = componentImportMatch[1];
1040
+ // Pattern 3: bare run(App, ...) — no result captured → const __sygnal = run(
1041
+ if (!binding) {
1042
+ inserts.push([runCall.index, 'const __sygnal = ']);
1043
+ return done(hmrBlock(componentPath, '__sygnal.hmr', '__sygnal.dispose'));
1044
+ }
1045
+ // Pattern 2: const app = run(App, ...)
1046
+ if (binding[0] !== '{') {
1047
+ return done(hmrBlock(componentPath, `${binding}.hmr`, `${binding}.dispose`));
1048
+ }
1049
+ // Pattern 1: const { hmr, dispose, ... } = run(App, ...) — only when both
1050
+ // are bound under their own names (a rename would leave them undefined)
1051
+ const names = binding.slice(1, -1).split(',').map(s => s.trim());
1052
+ if (names.includes('hmr') && names.includes('dispose')) {
1053
+ return done(hmrBlock(componentPath, 'hmr', 'dispose'));
1054
+ }
1055
+ return done();
1056
+ }
1057
+ }
1058
+ // PLAN-5 W-2 (D209): the adapter entries and the optional peer dependencies each one imports
1059
+ // (the Zag packages pinned together: they depend on each other's exact versions)
1060
+ const ZAG = '~1.45.0';
1061
+ const ADAPTER_PEERS = {
1062
+ react: ['react', 'react-dom'],
1063
+ zag: ['@zag-js/vanilla'],
1064
+ // D211: one subpath per Zag part, each needing only its own machine
1065
+ 'ui/menu': ['@zag-js/vanilla', '@zag-js/menu'],
1066
+ 'ui/select': ['@zag-js/vanilla', '@zag-js/select'],
1067
+ 'ui/combobox': ['@zag-js/vanilla', '@zag-js/combobox'],
1068
+ };
1069
+ // G-440: a dynamic import's specifier may be a template literal without substitutions
1070
+ const ADAPTER_RE = /(?:\bfrom|\bimport)\s*\(?\s*(['"`])sygnal\/(react|zag|ui\/(?:menu|select|combobox))\1/g;
1071
+ const ADAPTER_ID = /^sygnal\/(react|zag|ui\/(?:menu|select|combobox))$/;
1072
+ /**
1073
+ * The adapter entries an app module imports (statically, dynamically or in a re-export), or null.
1074
+ * G-415: real imports only. The module's AST from the plugin context's parse() (Vite and Rollup
1075
+ * provide it; the code is plain JS by then); when there is none, or the code doesn't parse, a
1076
+ * scan of the code with comments blanked whose `from` / `import` keyword isn't inside a string
1077
+ * or template literal. A literal that mentions an entry (a docs snippet) is never an import.
1078
+ */
1079
+ function adapterImports(ctx, code, id) {
1080
+ if (!code.includes('sygnal/') || id.includes('node_modules') || !/\.[cm]?[jt]sx?$/.test(cleanId(id)))
1081
+ return null;
1082
+ const noComments = blankOut(code, false);
1083
+ if (!ADAPTER_RE.test(noComments))
1084
+ return null;
1085
+ ADAPTER_RE.lastIndex = 0;
1086
+ const found = new Set();
1087
+ let ast = null;
1088
+ try {
1089
+ ast = typeof ctx?.parse == 'function' ? ctx.parse(code) : null;
1090
+ }
1091
+ catch (_) { }
1092
+ if (ast) {
1093
+ const walk = (n) => {
1094
+ if (!n || typeof n != 'object')
1095
+ return;
1096
+ if (Array.isArray(n))
1097
+ return n.forEach(walk);
1098
+ if (/^(Import|Export(Named|All))Declaration$|^ImportExpression$/.test(n.type)) {
1099
+ // G-440: import(`sygnal/zag`): a template literal without substitutions is a known specifier
1100
+ const src = n.source, q = src?.type == 'TemplateLiteral' && !src.expressions?.length ? src.quasis?.[0]?.value?.cooked : src?.value;
1101
+ const m = ADAPTER_ID.exec(q ?? '');
1102
+ if (m)
1103
+ found.add(m[1]);
340
1104
  }
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'));
1105
+ for (const k in n)
1106
+ if (k != 'type' && typeof n[k] == 'object')
1107
+ walk(n[k]);
1108
+ };
1109
+ walk(ast);
1110
+ }
1111
+ else {
1112
+ // strings and templates blanked too: a keyword there is text, not an import
1113
+ const noStrings = blankOut(code, true);
1114
+ for (const m of noComments.matchAll(ADAPTER_RE)) {
1115
+ if (noStrings.startsWith(m[0].slice(0, m[0][0] == 'f' ? 4 : 6), m.index))
1116
+ found.add(m[2]);
1117
+ }
1118
+ }
1119
+ return found.size ? [...found] : null;
1120
+ }
1121
+ /** SYG666: resolves each entry's peers from the importing module; reports the missing ones */
1122
+ async function checkPeers(ctx, entries, id) {
1123
+ for (const e of entries) {
1124
+ const missing = [];
1125
+ for (const peer of ADAPTER_PEERS[e]) {
1126
+ let r = null;
1127
+ try {
1128
+ r = await ctx.resolve(peer, id, { skipSelf: true });
346
1129
  }
347
- return done();
348
- },
349
- };
1130
+ catch (_) { }
1131
+ if (!r)
1132
+ missing.push(peer);
1133
+ }
1134
+ if (!missing.length)
1135
+ continue;
1136
+ const install = missing.map(m => (m.startsWith('@zag-js/') ? `${m}@${ZAG}` : m)).join(' ');
1137
+ ctx.error(`[Sygnal SYG666] 'sygnal/${e}' needs ${missing.join(', ')}, which ${missing.length > 1 ? "aren't" : "isn't"} installed: npm install ${install}.` +
1138
+ (e == 'react' ? " (For Preact: alias react, react-dom and react-dom/client to preact/compat.)" : '') +
1139
+ ' https://sygnal.js.org/reference/errors#syg666');
1140
+ }
350
1141
  }
351
1142
  /**
352
1143
  * Major version of the running Vite: from the plugin context (this.meta,
@@ -441,6 +1232,16 @@ function sygnalPackageDir(root) {
441
1232
  catch (_) { }
442
1233
  return undefined;
443
1234
  }
1235
+ /** The version of the 'sygnal' package the project resolves */
1236
+ function sygnalVersion(root) {
1237
+ const dir = sygnalPackageDir(root);
1238
+ try {
1239
+ return dir ? JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version : undefined;
1240
+ }
1241
+ catch (_) {
1242
+ return undefined;
1243
+ }
1244
+ }
444
1245
  /** Real paths of sygnal's 'astro/client' ESM file. */
445
1246
  function astroClientFiles(root) {
446
1247
  const files = new Set();
@@ -549,6 +1350,8 @@ async function startChecker(server, root, opts, defaults, explicit) {
549
1350
  const include = checkInclude(root, opts.include, (m) => logger.info(m));
550
1351
  const ignore = opts.ignore || defaults.ignore;
551
1352
  const strict = opts.strict === undefined ? defaults.strict : !!opts.strict;
1353
+ // D144: the a11y lane is a warning unless asked for as an error, under strict too
1354
+ const a11y = opts.a11y === 'error' ? 'error' : 'warn';
552
1355
  // Only error-severity findings open Vite's overlay: while an overlay is open
553
1356
  // Vite's client reloads the page on the first HMR update, so warnings stay
554
1357
  // in the terminal and the browser console. overlay: 'warn' is treated as
@@ -579,12 +1382,16 @@ async function startChecker(server, root, opts, defaults, explicit) {
579
1382
  const run = (initial = false) => {
580
1383
  let diags;
581
1384
  try {
582
- diags = mod.check(include, { cwd: root, strict, ignore });
1385
+ diags = mod.check(include, { cwd: root, strict, ignore, a11y });
583
1386
  }
584
1387
  catch (err) {
585
1388
  logger.warn(`[sygnal] sygnal-check failed: ${err?.message || err}`, { timestamp: true });
586
1389
  return;
587
1390
  }
1391
+ // A sygnal-check from before D144 reports SYG7xx as errors under strict: keep them warnings
1392
+ if (a11y !== 'error') {
1393
+ diags = diags.map(d => d && d.severity === 'error' && /^SYG7\d\d$/.test(d.code) ? { ...d, severity: 'warn' } : d);
1394
+ }
588
1395
  const shown = diags.filter(d => d.severity !== 'info');
589
1396
  const lines = shown.map(formatLine);
590
1397
  const key = lines.join('\n');
@@ -737,6 +1544,29 @@ function withSourcemap(code, inserts, tail, id) {
737
1544
  map: { version: 3, sources: [id], sourcesContent: [code], names: [], mappings },
738
1545
  };
739
1546
  }
1547
+ // A top-level (column 0) defineElement(...) call, as a statement or a declaration's value
1548
+ const DEFINE_ELEMENT_CALL_RE = /^(?:(?:export\s+)?(?:const|let|var)\s+[\w$]+\s*=\s*)?defineElement\s*\(/m;
1549
+ const ELEMENT_IMPORT_RE = /import\s+\{[^}]*\bdefineElement\b[^}]*\}\s+from\s+['"]sygnal\/element['"]/;
1550
+ /**
1551
+ * PLAN-4 GS-13: a module that defines custom elements with 'sygnal/element' accepts its own
1552
+ * hot updates. Re-running it calls defineElement again for the same tags, which swaps the
1553
+ * component in every live element and keeps each one's state; an edited component module it
1554
+ * imports reaches it the same way. Only for a top-level defineElement call, outside test
1555
+ * files, when the module has no HMR code of its own and doesn't import run() (an entry's
1556
+ * run() wiring decides there).
1557
+ */
1558
+ function elementHmr(code, noComments, id) {
1559
+ if (/\.(test|spec)\.[jt]sx?$/.test(id))
1560
+ return null;
1561
+ if (!ELEMENT_IMPORT_RE.test(noComments) || noComments.includes('import.meta.hot'))
1562
+ return null;
1563
+ if (!DEFINE_ELEMENT_CALL_RE.test(blankOut(code, true)))
1564
+ return null;
1565
+ return withSourcemap(code, [], ELEMENT_HMR_BLOCK, id);
1566
+ }
1567
+ const ELEMENT_HMR_BLOCK = `
1568
+ if (import.meta.hot) import.meta.hot.accept()
1569
+ `;
740
1570
  function hmrBlock(componentPath, hmrRef, disposeRef) {
741
1571
  return `
742
1572
  if (import.meta.hot) {
@@ -747,11 +1577,32 @@ if (import.meta.hot) {
747
1577
  }
748
1578
 
749
1579
  const SYGNAL_RENDERER_NAME = '@sygnal/astro';
1580
+ // PLAN-4 GS-11 / D120: both island entries import this; it re-exports the default export of the
1581
+ // module the `onError` option names (undefined without it)
1582
+ const ON_ERROR_ID = 'virtual:sygnal/astro-on-error';
1583
+ /** The Vite plugin serving ON_ERROR_ID; `file` is absolute (undefined: no hook) */
1584
+ function onErrorPlugin(file) {
1585
+ return {
1586
+ name: 'sygnal:astro-on-error',
1587
+ resolveId: (id) => id === ON_ERROR_ID ? '\0' + ON_ERROR_ID : undefined,
1588
+ load: (id) => id === '\0' + ON_ERROR_ID
1589
+ ? (file ? `export { default } from ${JSON.stringify(file)}` : 'export default undefined')
1590
+ : undefined,
1591
+ // Vite 6+ (Astro 6 renders in its own 'prerender' / 'ssr' environments): the island server
1592
+ // entry imports the virtual module, so Vite must load it (not Node). `ssr.noExternal`
1593
+ // below covers the 'ssr' environment and older Vite
1594
+ configEnvironment: (name) => name === 'client' ? undefined : { resolve: { noExternal: ['sygnal'] } },
1595
+ };
1596
+ }
1597
+ /** The project root as a directory path (Astro gives a file: URL) */
1598
+ const rootDir = (root) =>
1599
+ // @ts-ignore — Node's process
1600
+ !root ? process.cwd() : typeof root === 'string' ? root : node_url.fileURLToPath(root);
750
1601
  function sygnalAstroIntegration(options = {}) {
751
1602
  return {
752
1603
  name: SYGNAL_RENDERER_NAME,
753
1604
  hooks: {
754
- 'astro:config:setup': ({ addRenderer, updateConfig, command }) => {
1605
+ 'astro:config:setup': ({ addRenderer, updateConfig, command, config }) => {
755
1606
  addRenderer({
756
1607
  name: SYGNAL_RENDERER_NAME,
757
1608
  clientEntrypoint: 'sygnal/astro/client',
@@ -762,11 +1613,16 @@ function sygnalAstroIntegration(options = {}) {
762
1613
  jsx: 'automatic',
763
1614
  jsxImportSource: 'sygnal',
764
1615
  },
1616
+ // D120: the island entries import the virtual onError module, so they stay on Vite:
1617
+ // not pre-bundled with it, and not externalized (loaded by Node) during SSR
1618
+ optimizeDeps: { exclude: [ON_ERROR_ID] },
1619
+ ssr: { noExternal: ['sygnal'] },
765
1620
  };
1621
+ const hook = options.onError ? path.resolve(rootDir(config?.root), options.onError) : undefined;
766
1622
  // Dev mode (G-014): islands are started by sygnal/astro/client, which
767
1623
  // 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`.
1624
+ // in dev to install DevTools, set the dev flag and load the runtime checks first. It
1625
+ // also runs sygnal-check. `astro build` only gets the `globalthis` alias.
770
1626
  if (command === 'dev') {
771
1627
  vite.plugins = [sygnal({
772
1628
  disableJsx: true,
@@ -774,8 +1630,18 @@ function sygnalAstroIntegration(options = {}) {
774
1630
  vitestSetup: false,
775
1631
  diagnostics: options.diagnostics,
776
1632
  check: options.check,
1633
+ nativeGlobalThis: options.nativeGlobalThis,
1634
+ devtools: options.devtools,
777
1635
  })];
778
1636
  }
1637
+ else if (options.nativeGlobalThis !== false) {
1638
+ // G-099: the same `globalthis` alias the plugin adds in dev (R2-7: unless the
1639
+ // user's vite config already aliases `globalthis`)
1640
+ const alias = globalThisAlias(config?.vite?.resolve?.alias);
1641
+ if (alias.length)
1642
+ vite.resolve = { alias };
1643
+ }
1644
+ vite.plugins = [...(vite.plugins || []), onErrorPlugin(hook)];
779
1645
  updateConfig({ vite });
780
1646
  },
781
1647
  },