@marimo-team/islands 0.25.3-dev5 → 0.25.3-dev51

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 (220) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-Da5OoEUL.js} +14 -14
  2. package/dist/{ErrorBoundary-BiZWOrv4.js → ErrorBoundary-CbxCC2kk.js} +8 -8
  3. package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-Bz3mrprX.js} +2 -2
  4. package/dist/any-language-editor-k2B8YI3Y.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-Cs5ujb9K.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
  7. package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
  8. package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
  9. package/dist/{chat-ui-0lKdOoQ3.js → chat-ui-C1Gjgokv.js} +1103 -1103
  10. package/dist/{check-EDwSwP89.js → check-P35dc2dH.js} +1 -1
  11. package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-CcTSaxZt.js} +1 -1
  12. package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-DcJcjOaO.js} +1 -1
  13. package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BVtDTjaF.js} +1 -1
  14. package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-BkDFkh8q.js} +1 -1
  15. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  16. package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-CfAt7HT5.js} +2 -2
  17. package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CGEjNYa2.js} +2 -2
  18. package/dist/{code-visibility-CW5_8ZOI.js → code-visibility-DoZ9lxzx.js} +6 -6
  19. package/dist/{common-mzWw4FdI.js → common-D1PacyXO.js} +3112 -3015
  20. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-CPoxUG7G.js} +3 -3
  21. package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-BW6NmfJi.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-CWg3CTuF.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-CJmehSdc.js → data-grid-overlay-editor-B50UpkXd.js} +5 -5
  24. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  25. package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BPxtoS-z.js} +1 -1
  26. package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-DA-vJdFL.js} +1 -1
  27. package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-D6bkR84S.js} +1 -1
  28. package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-m60g4cAT.js} +1 -1
  29. package/dist/{dialog-CEBFZNyc.js → dialog-C1PcpCqX.js} +3 -3
  30. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  31. package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-MG5hiCIa.js} +76 -76
  32. package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-CK99sho2.js} +2 -2
  33. package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-Bryq0tdE.js} +1 -1
  34. package/dist/{error-banner-D-WCIiMt.js → error-banner-Sl2FlSJ4.js} +2 -2
  35. package/dist/{error-entries-DSvFWT_M.js → error-entries-CMNtA8NK.js} +959 -959
  36. package/dist/{feedback-button-BcT1FBud.js → feedback-button-DqPqwryB.js} +29 -29
  37. package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-D-3aRpfS.js} +2 -2
  38. package/dist/{formats-CeOmU1MU.js → formats-6pLWWrY2.js} +3 -3
  39. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  40. package/dist/{glide-data-editor-XckatD0j.js → glide-data-editor-BxMhUd3K.js} +370 -368
  41. package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Bt8_pBa2.js} +16 -16
  42. package/dist/{globals-jRrLCkrP.js → globals-DTJa_doJ.js} +3 -3
  43. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  44. package/dist/{html-to-image-B8s5SgGm.js → html-to-image-By72ALyI.js} +29 -29
  45. package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-s1C1kgfU.js} +1 -1
  46. package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-CCsRgolY.js} +1 -1
  47. package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-BrmxDlcc.js} +1 -1
  48. package/dist/{label-BDKF3Afc.js → label-BRnUC0NX.js} +2 -2
  49. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  50. package/dist/{loader-BQOfjTIJ.js → loader-Bm8cy3e8.js} +12 -12
  51. package/dist/main.js +2358 -2221
  52. package/dist/{mermaid-CHo49Yv-.js → mermaid-DKI0869I.js} +38 -38
  53. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  54. package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-641xJysF.js} +1 -1
  55. package/dist/{number-overlay-editor-pZx8dUDV.js → number-overlay-editor-Z_H6x5b-.js} +3 -3
  56. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  57. package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
  58. package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-keVUAUeS.js} +2 -2
  59. package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-DHktXPO_.js} +1 -1
  60. package/dist/precisionRound-DhAzeRcm.js +49 -0
  61. package/dist/{process-output-CKRXyszb.js → process-output-D-OMW5sV.js} +4 -4
  62. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  63. package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-BVEF4OV_.js} +2 -2
  64. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  65. package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CWo-UAKV.js} +1 -1
  66. package/dist/{reveal-component-79ez8-D5.js → reveal-component-tAB5K3QE.js} +10 -10
  67. package/dist/{select-BButFi-d.js → select-B0LRDH0g.js} +4 -4
  68. package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-CHQvLnq4.js} +1 -1
  69. package/dist/{spec-BwarBpUJ.js → spec-C-LRKGtT.js} +4 -4
  70. package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-BOJZgbUJ.js} +2 -2
  71. package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH-C53XXdYH.js} +2 -2
  72. package/dist/style.css +1 -1
  73. package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-D2o1gOtw.js} +1 -1
  74. package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BHFzN1Wk.js} +2 -2
  75. package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
  76. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  77. package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-rv7QUa46.js} +1 -1
  78. package/dist/{toDate-vIXljnsr.js → toDate-CjPuEPou.js} +21 -20
  79. package/dist/{toast-B9UMEi8u.js → toast-CFWpihJU.js} +4 -4
  80. package/dist/{tooltip-BO1REbqs.js → tooltip-CKO1pHJo.js} +2 -2
  81. package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-DBCITLeF.js} +1 -1
  82. package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-ChgNtdmE.js} +15 -14
  83. package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-c_yJtkAe.js} +2 -2
  84. package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Bet7kSsI.js} +1 -1
  85. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
  86. package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-VVmz_24o.js} +3 -3
  87. package/dist/{useTheme-CRnjkku0.js → useTheme-DympCsQg.js} +5 -5
  88. package/dist/{vega-component-DTG6zxaY.js → vega-component-DnfQeoyt.js} +147 -147
  89. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  90. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  91. package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-D36Zk7t-.js} +1 -1
  92. package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-BLBolvyQ.js} +1 -1
  93. package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CfH57l0v.js} +2 -2
  94. package/package.json +4 -4
  95. package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
  96. package/src/__tests__/host-imports.test.ts +1 -0
  97. package/src/__tests__/mount.test.ts +8 -8
  98. package/src/components/app-config/ai-config.tsx +5 -1
  99. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  100. package/src/components/chat/chat-panel.tsx +3 -7
  101. package/src/components/chat/chat-utils.ts +4 -8
  102. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  103. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  104. package/src/components/data-table/__tests__/export-actions.test.tsx +417 -6
  105. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  106. package/src/components/data-table/columns.tsx +2 -1
  107. package/src/components/data-table/context-menu.tsx +36 -20
  108. package/src/components/data-table/date-popover.tsx +6 -4
  109. package/src/components/data-table/export-actions.tsx +263 -161
  110. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
  111. package/src/components/data-table/schemas.ts +14 -36
  112. package/src/components/editor/Output.tsx +5 -1
  113. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  114. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
  115. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
  116. package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
  117. package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
  118. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  119. package/src/components/editor/cell/CreateCellButton.tsx +8 -10
  120. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  121. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  122. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  123. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +277 -78
  124. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +196 -41
  125. package/src/components/editor/connections/components.tsx +1 -1
  126. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  127. package/src/components/editor/controls/command-palette.tsx +22 -10
  128. package/src/components/editor/dynamic-favicon.tsx +21 -40
  129. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +139 -36
  130. package/src/components/editor/errors/send-error-report-button.tsx +10 -7
  131. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  132. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -22
  133. package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
  134. package/src/components/pages/home-page.tsx +4 -4
  135. package/src/components/slides/slide-cell-view.tsx +1 -1
  136. package/src/components/tracing/tracing.tsx +2 -2
  137. package/src/core/attachments/__tests__/toast.test.tsx +58 -0
  138. package/src/core/attachments/state.ts +5 -8
  139. package/src/core/attachments/toast.ts +15 -0
  140. package/src/core/cells/__tests__/error-report.test.ts +53 -3
  141. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  142. package/src/core/cells/__tests__/session.test.ts +18 -3
  143. package/src/core/cells/cells.ts +2 -1
  144. package/src/core/cells/error-report.ts +14 -6
  145. package/src/core/cells/pending-delete-service.ts +2 -6
  146. package/src/core/cells/session.ts +19 -1
  147. package/src/core/config/pair.ts +5 -3
  148. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  149. package/src/core/datasets/data-source-discovery.ts +4 -6
  150. package/src/core/edit-app.tsx +2 -2
  151. package/src/core/host/AGENTS.md +11 -2
  152. package/src/core/host/__tests__/attachments.test.ts +56 -0
  153. package/src/core/host/__tests__/document.test.ts +126 -0
  154. package/src/core/host/__tests__/events.test.ts +49 -0
  155. package/src/core/host/__tests__/fixtures.ts +41 -0
  156. package/src/core/host/__tests__/notebook-client.test.ts +496 -0
  157. package/src/core/host/connection.ts +81 -0
  158. package/src/core/host/events.ts +68 -0
  159. package/src/core/host/notebook-client.ts +316 -0
  160. package/src/core/host/resources/attachments.ts +19 -0
  161. package/src/core/host/resources/document.ts +101 -0
  162. package/src/core/hotkeys/actions.ts +2 -4
  163. package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
  164. package/src/core/islands/bootstrap.ts +0 -1
  165. package/src/core/network/connection.ts +4 -2
  166. package/src/core/run-app.tsx +2 -2
  167. package/src/core/runtime/__tests__/runtime.test.ts +5 -12
  168. package/src/core/runtime/runtime.ts +5 -5
  169. package/src/core/static/files.ts +2 -0
  170. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  171. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  172. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +149 -0
  173. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +168 -253
  174. package/src/core/websocket/connection-state.ts +70 -0
  175. package/src/core/websocket/state.ts +7 -0
  176. package/src/core/websocket/transports/__tests__/sse.test.ts +18 -4
  177. package/src/core/websocket/transports/create.ts +31 -0
  178. package/src/core/websocket/transports/sse.ts +14 -2
  179. package/src/core/websocket/transports/transport.ts +12 -23
  180. package/src/core/websocket/types.ts +9 -39
  181. package/src/core/websocket/useNotebookClient.ts +78 -0
  182. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +53 -233
  183. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  184. package/src/hooks/useAsyncData.ts +3 -3
  185. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  186. package/src/hooks/useElapsedTime.ts +3 -3
  187. package/src/hooks/useInputHistory.ts +4 -7
  188. package/src/hooks/useResizeObserver.ts +4 -7
  189. package/src/mount.tsx +3 -3
  190. package/src/plugins/core/registerReactComponent.tsx +1 -1
  191. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  192. package/src/plugins/impl/FormPlugin.tsx +1 -1
  193. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
  194. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
  195. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  196. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  197. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  198. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  199. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  200. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  201. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  202. package/src/plugins/impl/panel/PanelPlugin.tsx +15 -16
  203. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  204. package/src/plugins/impl/panel/utils.ts +17 -0
  205. package/src/plugins/impl/vega/make-selectable.ts +3 -6
  206. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  207. package/src/stories/editor.stories.tsx +24 -17
  208. package/src/utils/__tests__/arrays.test.ts +35 -0
  209. package/src/utils/__tests__/numbers.test.ts +48 -1
  210. package/src/utils/__tests__/objects.test.ts +21 -0
  211. package/src/utils/arrays.ts +10 -0
  212. package/src/utils/numbers.ts +28 -17
  213. package/src/utils/objects.ts +3 -7
  214. package/src/utils/once.ts +19 -19
  215. package/dist/any-language-editor-DZ_YTVus.js +0 -76
  216. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  217. package/dist/precisionRound-DMIVQrt2.js +0 -97
  218. package/src/core/host/__tests__/protocol.test.ts +0 -214
  219. package/src/core/host/protocol.ts +0 -1943
  220. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -7,9 +7,12 @@ import { userConfigAtom } from "@/core/config/config";
7
7
  import { parseUserConfig } from "@/core/config/config-schema";
8
8
  import { initialModeAtom } from "@/core/mode";
9
9
  import { store } from "@/core/state/jotai";
10
+ import type { DataURLString } from "@/utils/json/base64";
10
11
  import { Model } from "../model";
11
12
  import type { Host } from "../host";
12
- import type { ModelState } from "../types";
13
+ import type { ModelState, WidgetModelId } from "../types";
14
+ import { WidgetRuntime } from "../runtime";
15
+ import { Deferred } from "@/utils/Deferred";
13
16
  import { visibleForTesting } from "../widget-binding";
14
17
 
15
18
  const { WidgetDefRegistry, WidgetBinding } = visibleForTesting;
@@ -30,13 +33,14 @@ const createTestHost = (): Host => ({
30
33
  function createBinding<T extends ModelState>(
31
34
  widgetDef: AnyWidget<T>,
32
35
  model: Model<T>,
33
- options: { controller?: AbortController } = {},
36
+ options: { controller?: AbortController; onDestroy?: () => void } = {},
34
37
  ) {
35
38
  return WidgetBinding.create({
36
39
  widgetDef,
37
40
  model,
38
41
  createHost: createTestHost,
39
42
  controller: options.controller,
43
+ onDestroy: options.onDestroy,
40
44
  });
41
45
  }
42
46
 
@@ -123,6 +127,25 @@ describe("WidgetDefRegistry", () => {
123
127
  promise2.catch(() => undefined);
124
128
  });
125
129
 
130
+ it("does not evict a replacement when an invalidated import fails", async () => {
131
+ const oldImport = getModule(
132
+ registry,
133
+ "http://localhost/old.js",
134
+ "replaced",
135
+ );
136
+ registry.invalidate("replaced");
137
+ const newImport = registry.getModule({
138
+ jsUrl: "data:text/javascript,export default {}",
139
+ jsHash: "replaced",
140
+ kernelAuthored: true,
141
+ });
142
+ await expect(oldImport).rejects.toThrow(/untrusted/i);
143
+ await newImport;
144
+ expect(getModule(registry, "http://localhost/new.js", "replaced")).toBe(
145
+ newImport,
146
+ );
147
+ });
148
+
126
149
  describe("URL validation", () => {
127
150
  it.each([
128
151
  // Attack vector: raw <marimo-anywidget data-js-url=...> in markdown
@@ -148,9 +171,225 @@ describe("WidgetDefRegistry", () => {
148
171
  ).rejects.not.toThrow(/untrusted/i);
149
172
  });
150
173
  });
174
+
175
+ describe("static module URLs", () => {
176
+ afterEach(() => {
177
+ delete window.__MARIMO_STATIC__;
178
+ vi.restoreAllMocks();
179
+ });
180
+
181
+ it("releases the generated URL when a static module fails to import", async () => {
182
+ window.__MARIMO_STATIC__ = {
183
+ files: {
184
+ "/@file/widget.js":
185
+ "data:text/javascript;base64,ZXhwb3J0IHt9" as DataURLString,
186
+ },
187
+ };
188
+ const create = vi
189
+ .spyOn(URL, "createObjectURL")
190
+ .mockReturnValue("blob:widget-module");
191
+ const revoke = vi
192
+ .spyOn(URL, "revokeObjectURL")
193
+ .mockImplementation(() => undefined);
194
+
195
+ await expect(
196
+ getModule(registry, "./@file/widget.js", "static-module"),
197
+ ).rejects.toThrow(/blob:/);
198
+
199
+ expect(create).toHaveBeenCalledOnce();
200
+ expect(revoke).toHaveBeenCalledExactlyOnceWith("blob:widget-module");
201
+ });
202
+
203
+ it("does not revoke active modules when their cache entry is invalidated", async () => {
204
+ window.__MARIMO_STATIC__ = {
205
+ files: {
206
+ "/@file/widget.js":
207
+ "data:text/javascript;base64,ZXhwb3J0IHt9" as DataURLString,
208
+ },
209
+ };
210
+ const oldUrl = "data:text/javascript,export const version = 1";
211
+ const newUrl = "data:text/javascript,export const version = 2";
212
+ vi.spyOn(URL, "createObjectURL")
213
+ .mockReturnValueOnce(oldUrl)
214
+ .mockReturnValueOnce(newUrl);
215
+ const revoke = vi
216
+ .spyOn(URL, "revokeObjectURL")
217
+ .mockImplementation(() => undefined);
218
+ const oldLifetime = new AbortController();
219
+ const newLifetime = new AbortController();
220
+ const options = { jsUrl: "./@file/widget.js", jsHash: "invalidated" };
221
+ const oldImport = registry.getModule({
222
+ ...options,
223
+ lifetime: oldLifetime.signal,
224
+ });
225
+ registry.invalidate(options.jsHash);
226
+ const newImport = registry.getModule({
227
+ ...options,
228
+ lifetime: newLifetime.signal,
229
+ });
230
+ await Promise.all([oldImport, newImport]);
231
+ expect(revoke).not.toHaveBeenCalled();
232
+ oldLifetime.abort();
233
+ await vi.waitFor(() =>
234
+ expect(revoke).toHaveBeenCalledExactlyOnceWith(oldUrl),
235
+ );
236
+ const siblingLifetime = new AbortController();
237
+ expect(
238
+ registry.getModule({ ...options, lifetime: siblingLifetime.signal }),
239
+ ).toBe(newImport);
240
+ newLifetime.abort();
241
+ siblingLifetime.abort();
242
+ await vi.waitFor(() => expect(revoke).toHaveBeenLastCalledWith(newUrl));
243
+ });
244
+
245
+ it("waits for a canceled import to settle before revoking its URL", async () => {
246
+ window.__MARIMO_STATIC__ = {
247
+ files: {
248
+ "/@file/widget.js":
249
+ "data:text/javascript;base64,ZXhwb3J0IHt9" as DataURLString,
250
+ },
251
+ };
252
+ const moduleUrl = "data:text/javascript,export const pending = true";
253
+ vi.spyOn(URL, "createObjectURL").mockReturnValue(moduleUrl);
254
+ const revoke = vi
255
+ .spyOn(URL, "revokeObjectURL")
256
+ .mockImplementation(() => undefined);
257
+ const lifetime = new AbortController();
258
+ const pending = registry.getModule({
259
+ jsUrl: "./@file/widget.js",
260
+ jsHash: "pending",
261
+ lifetime: lifetime.signal,
262
+ });
263
+ lifetime.abort();
264
+ expect(revoke).not.toHaveBeenCalled();
265
+ await pending;
266
+ expect(revoke).toHaveBeenCalledExactlyOnceWith(moduleUrl);
267
+ });
268
+
269
+ it("does not revoke an unchanged URL for a missing static file", async () => {
270
+ window.__MARIMO_STATIC__ = { files: {} };
271
+ const revoke = vi
272
+ .spyOn(URL, "revokeObjectURL")
273
+ .mockImplementation(() => undefined);
274
+
275
+ await expect(
276
+ getModule(registry, "./@file/missing.js", "missing-module"),
277
+ ).rejects.toThrow(/http:/);
278
+
279
+ expect(revoke).not.toHaveBeenCalled();
280
+ });
281
+
282
+ it("keeps a shared module URL until the last widget generation is destroyed", async () => {
283
+ window.__MARIMO_STATIC__ = {
284
+ files: {
285
+ "/@file/widget.js":
286
+ "data:text/javascript;base64,ZXhwb3J0IHt9" as DataURLString,
287
+ },
288
+ };
289
+ // Node cannot import blob URLs; a data URL exercises successful loading.
290
+ const moduleUrl = `data:text/javascript,${encodeURIComponent(
291
+ "export default { initialize: () => ({ moduleUrl: import.meta.url }) }",
292
+ )}`;
293
+ vi.spyOn(URL, "createObjectURL").mockReturnValue(moduleUrl);
294
+ const revoke = vi
295
+ .spyOn(URL, "revokeObjectURL")
296
+ .mockImplementation(() => undefined);
297
+
298
+ const createRuntime = (id: string) => {
299
+ const runtime = new WidgetRuntime(id as WidgetModelId, {
300
+ timeout: 50,
301
+ isEditMode: () => true,
302
+ createHost: createTestHost,
303
+ onModelTimeout: vi.fn(),
304
+ });
305
+ runtime.setModel(new Model({}, createMockComm()));
306
+ runtime.setSpec({ url: "./@file/widget.js", hash: "static-success" });
307
+ return runtime;
308
+ };
309
+ const first = createRuntime("first");
310
+ const second = createRuntime("second");
311
+ const widgets = await Promise.all([
312
+ first.getWidget(),
313
+ second.getWidget(),
314
+ ]);
315
+ expect(widgets.map((widget) => widget.exports)).toEqual([
316
+ { moduleUrl },
317
+ { moduleUrl },
318
+ ]);
319
+ expect(URL.createObjectURL).toHaveBeenCalledOnce();
320
+ expect(revoke).not.toHaveBeenCalled();
321
+
322
+ first.dispose();
323
+ await new Promise((resolve) => setTimeout(resolve, 0));
324
+ expect(revoke).not.toHaveBeenCalled();
325
+
326
+ second.dispose();
327
+ await vi.waitFor(() => {
328
+ expect(revoke).toHaveBeenCalledExactlyOnceWith(moduleUrl);
329
+ });
330
+
331
+ // A later generation reuses the evaluated module rather than
332
+ // re-running its top-level side effects, and does not revoke again.
333
+ const third = createRuntime("third");
334
+ const widget = await third.getWidget();
335
+ expect(widget.exports).toEqual({ moduleUrl });
336
+ expect(URL.createObjectURL).toHaveBeenCalledOnce();
337
+ third.dispose();
338
+ await new Promise((resolve) => setTimeout(resolve, 0));
339
+ expect(revoke).toHaveBeenCalledOnce();
340
+ });
341
+ });
151
342
  });
152
343
 
153
344
  describe("WidgetBinding", () => {
345
+ it("releases module resources after asynchronous generation cleanup", async () => {
346
+ const cleanup = new Deferred<void>();
347
+ const onDestroy = vi.fn();
348
+ const binding = await createBinding(
349
+ { initialize: () => () => cleanup.promise },
350
+ new Model({}, createMockComm()),
351
+ { onDestroy },
352
+ );
353
+ const destroying = binding.destroy();
354
+ expect(onDestroy).not.toHaveBeenCalled();
355
+ cleanup.resolve();
356
+ await destroying;
357
+ await binding.destroy();
358
+ expect(onDestroy).toHaveBeenCalledOnce();
359
+ });
360
+
361
+ it("retains module resources until canceled initialization and late cleanup settle", async () => {
362
+ const initialize = new Deferred<() => Promise<void>>();
363
+ const cleanup = new Deferred<void>();
364
+ const controller = new AbortController();
365
+ const onDestroy = vi.fn();
366
+ const pending = createBinding(
367
+ { initialize: () => initialize.promise },
368
+ new Model({}, createMockComm()),
369
+ { controller, onDestroy },
370
+ );
371
+ controller.abort();
372
+ await expect(pending).rejects.toThrow(/binding destroyed/);
373
+ expect(onDestroy).not.toHaveBeenCalled();
374
+ initialize.resolve(() => cleanup.promise);
375
+ await Promise.resolve();
376
+ expect(onDestroy).not.toHaveBeenCalled();
377
+ cleanup.resolve();
378
+ await vi.waitFor(() => expect(onDestroy).toHaveBeenCalledOnce());
379
+ });
380
+
381
+ it("releases module resources when initialization fails", async () => {
382
+ const onDestroy = vi.fn();
383
+ await expect(
384
+ createBinding(
385
+ { initialize: () => Promise.reject(new Error("initialize failed")) },
386
+ new Model({}, createMockComm()),
387
+ { onDestroy },
388
+ ),
389
+ ).rejects.toThrow("initialize failed");
390
+ await vi.waitFor(() => expect(onDestroy).toHaveBeenCalledOnce());
391
+ });
392
+
154
393
  it("should run initialize exactly once and expose exports", async () => {
155
394
  const model = new Model<ModelState>({ count: 0 }, createMockComm());
156
395
  const exports = { setValue: vi.fn() };
@@ -206,21 +206,33 @@ export class WidgetRuntime {
206
206
  controller: AbortController;
207
207
  }): Promise<WidgetBinding<ModelState>> {
208
208
  const { spec, model, controller } = options;
209
- const mod = await WIDGET_DEF_REGISTRY.getModule({
210
- jsUrl: spec.url,
211
- jsHash: spec.hash,
212
- kernelAuthored: true,
213
- });
214
- const widget = resolveAnyWidget(mod, spec.url);
215
- if (!widget) {
216
- WIDGET_DEF_REGISTRY.invalidate(spec.hash);
217
- throw getInvalidAnyWidgetModuleError(mod, spec.url);
209
+ const moduleLifetime = new AbortController();
210
+ let widget: NonNullable<ReturnType<typeof resolveAnyWidget>>;
211
+ try {
212
+ const mod = await WIDGET_DEF_REGISTRY.getModule({
213
+ jsUrl: spec.url,
214
+ jsHash: spec.hash,
215
+ kernelAuthored: true,
216
+ lifetime: moduleLifetime.signal,
217
+ });
218
+ const resolved = resolveAnyWidget(mod, spec.url);
219
+ if (!resolved) {
220
+ WIDGET_DEF_REGISTRY.invalidate(spec.hash);
221
+ throw getInvalidAnyWidgetModuleError(mod, spec.url);
222
+ }
223
+ widget = resolved;
224
+ } catch (error) {
225
+ moduleLifetime.abort();
226
+ throw error;
218
227
  }
228
+ // From here WidgetBinding owns release, including late cleanup when the
229
+ // generation is destroyed during initialize.
219
230
  return WidgetBinding.create({
220
231
  widgetDef: widget,
221
232
  model,
222
233
  createHost: this.#createHost,
223
234
  controller,
235
+ onDestroy: () => moduleLifetime.abort(),
224
236
  });
225
237
  }
226
238
 
@@ -46,38 +46,80 @@ function abortSignalAny(signals: AbortSignal[]): AbortSignal {
46
46
  return controller.signal;
47
47
  }
48
48
 
49
+ interface ModuleEntry {
50
+ promise: Promise<unknown>;
51
+ references: number;
52
+ dispose: () => void;
53
+ }
54
+
49
55
  /**
50
56
  * Deduplicates ESM imports by jsHash.
51
57
  * A single import is shared across all widget instances using the same module.
52
58
  */
53
59
  class WidgetDefRegistry {
54
- #cache = new Map<string, Promise<unknown>>();
60
+ #cache = new Map<string, ModuleEntry>();
55
61
 
56
62
  /**
57
63
  * Get (or start) the ESM import for a widget module, cached by
58
64
  * jsHash so multiple instances share one import. Pass
59
65
  * `kernelAuthored: true` only for URLs from an `EsmSpec`; it widens
60
- * the import gate to remote and data URLs.
66
+ * the import gate to remote and data URLs. A lifetime retains the module's
67
+ * temporary URL until that signal aborts; omit it to retain the URL for
68
+ * the notebook's lifetime. The evaluated module itself stays cached.
61
69
  */
62
70
  public getModule(options: {
63
71
  jsUrl: string;
64
72
  jsHash: string;
65
73
  kernelAuthored?: boolean;
74
+ lifetime?: AbortSignal;
66
75
  }): Promise<unknown> {
67
- const { jsUrl, jsHash, kernelAuthored = false } = options;
68
- const cached = this.#cache.get(jsHash);
69
- if (cached) {
70
- return cached;
76
+ const { jsUrl, jsHash, kernelAuthored = false, lifetime } = options;
77
+ let entry = this.#cache.get(jsHash);
78
+ if (!entry) {
79
+ const imported = this.#doImport(jsUrl, { kernelAuthored });
80
+ const created: ModuleEntry = {
81
+ references: 0,
82
+ dispose: () => undefined,
83
+ promise: imported.then(
84
+ ({ module, dispose }) => {
85
+ created.dispose = dispose;
86
+ return module;
87
+ },
88
+ (error) => {
89
+ // An invalidated import must not evict its replacement.
90
+ if (this.#cache.get(jsHash) === created) {
91
+ this.#cache.delete(jsHash);
92
+ }
93
+ throw error;
94
+ },
95
+ ),
96
+ };
97
+ this.#cache.set(jsHash, created);
98
+ entry = created;
71
99
  }
72
-
73
- const promise = this.#doImport(jsUrl, { kernelAuthored }).catch((error) => {
74
- // On failure, remove from cache so a retry with a new URL can work
75
- this.#cache.delete(jsHash);
76
- throw error;
77
- });
78
-
79
- this.#cache.set(jsHash, promise);
80
- return promise;
100
+ const retained = entry;
101
+ retained.references += 1;
102
+ const release = () => {
103
+ retained.references -= 1;
104
+ if (retained.references === 0) {
105
+ // Keep the evaluated module cached: re-importing would re-run its
106
+ // top-level side effects (e.g. `customElements.define`). Only the
107
+ // temporary URL is released.
108
+ void retained.promise.then(
109
+ () => {
110
+ retained.dispose();
111
+ retained.dispose = () => undefined;
112
+ },
113
+ () => undefined,
114
+ );
115
+ }
116
+ };
117
+ if (lifetime?.aborted) {
118
+ release();
119
+ } else {
120
+ lifetime?.addEventListener("abort", release, { once: true });
121
+ }
122
+ return retained.promise;
81
123
  }
82
124
 
83
125
  public invalidate(jsHash: string): void {
@@ -87,7 +129,7 @@ class WidgetDefRegistry {
87
129
  async #doImport(
88
130
  jsUrl: string,
89
131
  opts: { kernelAuthored?: boolean } = {},
90
- ): Promise<unknown> {
132
+ ): Promise<{ module: unknown; dispose: () => void }> {
91
133
  // By default, only trust marimo virtual file paths: arbitrary URLs
92
134
  // would let markdown-injected elements import attacker-controlled
93
135
  // JavaScript. Kernel-authored URLs (from an `EsmSpec`) are exempt —
@@ -103,11 +145,23 @@ class WidgetDefRegistry {
103
145
  )}`,
104
146
  );
105
147
  }
106
- let url = asRemoteURL(jsUrl).toString();
107
- if (isStaticNotebook()) {
108
- url = resolveVirtualFileURL(url);
148
+ const remoteUrl = asRemoteURL(jsUrl).toString();
149
+ const url = isStaticNotebook()
150
+ ? resolveVirtualFileURL(remoteUrl)
151
+ : remoteUrl;
152
+ const dispose = () => {
153
+ // Only the static-file resolver's newly created URL belongs to us.
154
+ if (url !== remoteUrl) {
155
+ URL.revokeObjectURL(url);
156
+ }
157
+ };
158
+ try {
159
+ const module = await import(/* @vite-ignore */ url);
160
+ return { module, dispose };
161
+ } catch (error) {
162
+ dispose();
163
+ throw error;
109
164
  }
110
- return import(/* @vite-ignore */ url);
111
165
  }
112
166
  }
113
167
 
@@ -132,6 +186,7 @@ interface CreateWidgetBindingOptions<T extends ModelState> {
132
186
  model: Model<T>;
133
187
  createHost: HostFactory;
134
188
  controller?: AbortController;
189
+ onDestroy?: () => void;
135
190
  }
136
191
 
137
192
  async function safelyRunCleanup(cleanup: Cleanup, reason: string) {
@@ -159,6 +214,7 @@ export class WidgetBinding<T extends ModelState = ModelState> {
159
214
  #createHost: HostFactory;
160
215
  #cleanupTasks = new Set<Promise<void>>();
161
216
  #viewTasks = new Set<Promise<void>>();
217
+ #onDestroy?: () => void;
162
218
 
163
219
  private constructor(options: {
164
220
  widget: ResolvedWidget<T>;
@@ -166,12 +222,14 @@ export class WidgetBinding<T extends ModelState = ModelState> {
166
222
  exports: unknown;
167
223
  controller: AbortController;
168
224
  createHost: HostFactory;
225
+ onDestroy?: () => void;
169
226
  }) {
170
227
  this.#widget = options.widget;
171
228
  this.#model = options.model;
172
229
  this.#exports = options.exports;
173
230
  this.#controller = options.controller;
174
231
  this.#createHost = options.createHost;
232
+ this.#onDestroy = options.onDestroy;
175
233
  }
176
234
 
177
235
  /**
@@ -185,76 +243,94 @@ export class WidgetBinding<T extends ModelState = ModelState> {
185
243
  model,
186
244
  createHost,
187
245
  controller = new AbortController(),
246
+ onDestroy,
188
247
  }: CreateWidgetBindingOptions<T>): Promise<WidgetBinding<T>> {
189
248
  const signal = controller.signal;
190
- const widget: ResolvedWidget<T> =
191
- typeof widgetDef === "function" ? await widgetDef() : widgetDef;
249
+ let lateCleanup: Promise<void> | undefined;
250
+ try {
251
+ const widget: ResolvedWidget<T> =
252
+ typeof widgetDef === "function" ? await widgetDef() : widgetDef;
192
253
 
193
- const initPromise = Promise.resolve(
194
- widget.initialize?.({
195
- model: modelProxy(model, signal),
196
- experimental,
197
- signal,
198
- }),
199
- );
200
- // If destroyed mid-initialize, still run a late-arriving legacy
201
- // cleanup callback: the widget acquired resources.
202
- initPromise
203
- .then(async (settled) => {
204
- if (signal.aborted && isCleanup(settled)) {
205
- await safelyRunCleanup(settled, "late initialize");
206
- }
207
- })
208
- .catch(() => undefined);
254
+ if (signal.aborted) {
255
+ throw new Error("[anywidget] binding destroyed");
256
+ }
257
+ const initPromise = Promise.resolve(
258
+ widget.initialize?.({
259
+ model: modelProxy(model, signal),
260
+ experimental,
261
+ signal,
262
+ }),
263
+ );
264
+ // If destroyed mid-initialize, still run a late-arriving legacy
265
+ // cleanup callback: the widget acquired resources.
266
+ lateCleanup = initPromise
267
+ .then(async (settled) => {
268
+ if (signal.aborted && isCleanup(settled)) {
269
+ await safelyRunCleanup(settled, "late initialize");
270
+ }
271
+ })
272
+ .catch(() => undefined);
209
273
 
210
- // Race against destruction so a hung `initialize` can't strand
211
- // callers whose model already closed.
212
- const result = await Promise.race([
213
- initPromise,
214
- new Promise<never>((_, reject) => {
215
- const abort = () => reject(new Error("[anywidget] binding destroyed"));
216
- if (signal.aborted) {
217
- abort();
218
- return;
219
- }
220
- signal.addEventListener("abort", abort, { once: true });
221
- }),
222
- ]);
274
+ // Race against destruction so a hung `initialize` can't strand
275
+ // callers whose model already closed.
276
+ const result = await Promise.race([
277
+ initPromise,
278
+ new Promise<never>((_, reject) => {
279
+ const abort = () =>
280
+ reject(new Error("[anywidget] binding destroyed"));
281
+ if (signal.aborted) {
282
+ abort();
283
+ return;
284
+ }
285
+ signal.addEventListener("abort", abort, { once: true });
286
+ }),
287
+ ]);
223
288
 
224
- if (signal.aborted) {
225
- throw new Error("[anywidget] binding destroyed");
226
- }
289
+ if (signal.aborted) {
290
+ throw new Error("[anywidget] binding destroyed");
291
+ }
227
292
 
228
- // Distinguish anywidget's three return shapes:
229
- // function → legacy cleanup callback (run on destroy)
230
- // object → widget exports (anywidget>=0.11)
231
- // void → nothing
232
- let exports: unknown;
233
- let initializeCleanup: Cleanup | undefined;
234
- if (isCleanup(result)) {
235
- initializeCleanup = result;
236
- exports = undefined;
237
- } else if (typeof result === "object" && result !== null) {
238
- exports = result;
239
- } else {
240
- exports = undefined;
241
- }
293
+ // Distinguish anywidget's three return shapes:
294
+ // function → legacy cleanup callback (run on destroy)
295
+ // object → widget exports (anywidget>=0.11)
296
+ // void → nothing
297
+ let exports: unknown;
298
+ let initializeCleanup: Cleanup | undefined;
299
+ if (isCleanup(result)) {
300
+ initializeCleanup = result;
301
+ exports = undefined;
302
+ } else if (typeof result === "object" && result !== null) {
303
+ exports = result;
304
+ } else {
305
+ exports = undefined;
306
+ }
242
307
 
243
- const binding = new WidgetBinding({
244
- widget,
245
- model,
246
- exports,
247
- controller,
248
- createHost,
249
- });
250
- if (initializeCleanup) {
251
- signal.addEventListener(
252
- "abort",
253
- () => void binding.#trackCleanup(initializeCleanup, "initialize"),
254
- { once: true },
255
- );
308
+ const binding = new WidgetBinding({
309
+ widget,
310
+ model,
311
+ exports,
312
+ controller,
313
+ createHost,
314
+ onDestroy,
315
+ });
316
+ if (initializeCleanup) {
317
+ signal.addEventListener(
318
+ "abort",
319
+ () => void binding.#trackCleanup(initializeCleanup, "initialize"),
320
+ { once: true },
321
+ );
322
+ }
323
+ return binding;
324
+ } catch (error) {
325
+ // A canceled initialize may still be using import.meta.url in its
326
+ // late cleanup. Keep the module alive until that work settles.
327
+ if (lateCleanup) {
328
+ void lateCleanup.then(() => onDestroy?.());
329
+ } else {
330
+ onDestroy?.();
331
+ }
332
+ throw error;
256
333
  }
257
- return binding;
258
334
  }
259
335
 
260
336
  /**
@@ -353,6 +429,8 @@ export class WidgetBinding<T extends ModelState = ModelState> {
353
429
  while (this.#cleanupTasks.size > 0) {
354
430
  await Promise.all(this.#cleanupTasks);
355
431
  }
432
+ this.#onDestroy?.();
433
+ this.#onDestroy = undefined;
356
434
  }
357
435
  }
358
436
 
@@ -53,14 +53,14 @@ const AnyLanguageCodeMirror: React.FC<
53
53
  extensions?: readonly Extension[];
54
54
  }
55
55
  > = ({
56
- language,
56
+ language: requestedLanguage,
57
57
  hideUnsupportedLanguageErrors,
58
58
  showCopyButton,
59
59
  extensions = EMPTY_EXTENSIONS,
60
60
  ...props
61
61
  }) => {
62
62
  // Maybe normalize the language to the extension
63
- language = LANGUAGE_MAP[language || ""] || language;
63
+ const language = LANGUAGE_MAP[requestedLanguage || ""] || requestedLanguage;
64
64
 
65
65
  const isSupported = isSupportedLanguage(language);
66
66
  if (!isSupported) {