@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev61

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 (171) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
  2. package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
  3. package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
  4. package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
  5. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  6. package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
  7. package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
  8. package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
  9. package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
  10. package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
  11. package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
  12. package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
  13. package/dist/main.js +3630 -1801
  14. package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
  15. package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
  16. package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
  17. package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
  18. package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
  19. package/dist/style.css +1 -1
  20. package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
  21. package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
  22. package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
  23. package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
  24. package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
  25. package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
  26. package/package.json +4 -4
  27. package/src/__mocks__/requests.ts +1 -0
  28. package/src/__tests__/CellStatus.test.tsx +1 -2
  29. package/src/components/app-config/user-config-form.tsx +52 -0
  30. package/src/components/chat/acp/agent-panel.tsx +35 -1
  31. package/src/components/chat/chat-panel.tsx +68 -29
  32. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  33. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  34. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  35. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  36. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  37. package/src/components/data-table/charts/charts.tsx +23 -1
  38. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  39. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  40. package/src/components/debugger/debugger-code.tsx +7 -2
  41. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  42. package/src/components/editor/cell/CellStatus.tsx +1 -20
  43. package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
  44. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  45. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  46. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  47. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  48. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  49. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  50. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  51. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  52. package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
  53. package/src/components/editor/errors/auto-fix.tsx +88 -34
  54. package/src/components/editor/errors/fix-mode.ts +1 -1
  55. package/src/components/editor/notebook-cell.tsx +7 -0
  56. package/src/components/editor/output/ImageOutput.tsx +20 -6
  57. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
  58. package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
  59. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  60. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  61. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  62. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  63. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  64. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  65. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  66. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  67. package/src/components/slides/minimap.tsx +7 -1
  68. package/src/components/slides/reveal-component.tsx +3 -0
  69. package/src/components/slides/slide-cell-view.tsx +10 -6
  70. package/src/components/slides/slide.tsx +16 -13
  71. package/src/components/tracing/tracing.tsx +2 -1
  72. package/src/core/ai/config.ts +2 -2
  73. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  74. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  75. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  76. package/src/core/ai/context/providers/datasource.ts +27 -2
  77. package/src/core/ai/context/providers/error.ts +226 -36
  78. package/src/core/ai/context/registry.ts +77 -43
  79. package/src/core/ai/state.ts +11 -0
  80. package/src/core/cells/__tests__/cell.test.ts +39 -0
  81. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  82. package/src/core/cells/cell.ts +5 -3
  83. package/src/core/cells/readonly-code-display.ts +35 -0
  84. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  85. package/src/core/codemirror/ai/resources.ts +15 -10
  86. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  87. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
  88. package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
  89. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  90. package/src/core/codemirror/cells/debugger-state.ts +133 -0
  91. package/src/core/codemirror/cells/extensions.ts +32 -4
  92. package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
  93. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  94. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  95. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  96. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  97. package/src/core/codemirror/utils.ts +15 -0
  98. package/src/core/config/__tests__/config-schema.test.ts +17 -0
  99. package/src/core/config/config-schema.ts +1 -0
  100. package/src/core/config/config.ts +10 -0
  101. package/src/core/config/feature-flag.tsx +4 -0
  102. package/src/core/edit-app.tsx +8 -5
  103. package/src/core/errors/errors.ts +2 -1
  104. package/src/core/islands/bootstrap.ts +4 -3
  105. package/src/core/islands/bridge.ts +1 -0
  106. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  107. package/src/core/lifecycle/render-policy.ts +125 -0
  108. package/src/core/mime.ts +11 -4
  109. package/src/core/network/__tests__/api.test.ts +17 -0
  110. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  111. package/src/core/network/api.ts +17 -6
  112. package/src/core/network/requests-lazy.ts +1 -0
  113. package/src/core/network/requests-network.ts +8 -0
  114. package/src/core/network/requests-static.ts +1 -0
  115. package/src/core/network/requests-toasting.tsx +10 -1
  116. package/src/core/network/types.ts +2 -0
  117. package/src/core/run-app.tsx +27 -23
  118. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  119. package/src/core/runtime/__tests__/runtime.test.ts +45 -0
  120. package/src/core/runtime/adapter.ts +182 -0
  121. package/src/core/runtime/runtime.ts +30 -16
  122. package/src/core/wasm/PyodideLoader.tsx +20 -62
  123. package/src/core/wasm/bridge.ts +13 -4
  124. package/src/core/wasm/state.ts +8 -19
  125. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
  126. package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
  127. package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
  128. package/src/core/websocket/transports/sse.ts +308 -0
  129. package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
  130. package/src/core/websocket/useWebSocket.tsx +12 -2
  131. package/src/css/app/codemirror.css +14 -0
  132. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  133. package/src/hooks/useDelayElapsed.ts +27 -0
  134. package/src/mount.tsx +3 -3
  135. package/src/plugins/core/RenderHTML.tsx +35 -1
  136. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  137. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  138. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
  139. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
  140. package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
  141. package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
  142. package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
  143. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
  144. package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
  145. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
  146. package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
  147. package/src/plugins/impl/anywidget/host.ts +54 -0
  148. package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
  149. package/src/plugins/impl/anywidget/model.ts +59 -239
  150. package/src/plugins/impl/anywidget/registry.ts +268 -0
  151. package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
  152. package/src/plugins/impl/anywidget/runtime.ts +422 -0
  153. package/src/plugins/impl/anywidget/types.ts +14 -0
  154. package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
  155. package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
  156. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  157. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  158. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  159. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
  160. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
  161. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
  162. package/src/utils/errors.ts +15 -0
  163. package/src/utils/time.ts +20 -0
  164. package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
  165. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  166. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  167. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  168. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  169. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  170. package/src/core/wasm/__tests__/state.test.ts +0 -124
  171. package/src/plugins/impl/anywidget/schemas.ts +0 -32
@@ -0,0 +1,268 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { NotificationMessageData } from "@/core/kernel/messages";
3
+ import { getRequestClient } from "@/core/network/requests";
4
+ import { isStaticNotebook } from "@/core/static/static-state";
5
+ import { assertNever } from "@/utils/assertNever";
6
+ import {
7
+ type Base64String,
8
+ base64ToDataView,
9
+ dataViewToBase64,
10
+ } from "@/utils/json/base64";
11
+ import { Logger } from "@/utils/Logger";
12
+ import { repl } from "@/utils/repl";
13
+ import { viewStateAtom } from "@/core/mode";
14
+ import { store } from "@/core/state/jotai";
15
+ import { createHost, type WidgetResolver } from "./host";
16
+ import { getMarimoInternal, type MarimoComm, Model } from "./model";
17
+ import { WidgetRuntime } from "./runtime";
18
+ import { decodeFromWire, serializeBuffersToBase64 } from "./serialization";
19
+ import {
20
+ isWidgetModelId,
21
+ type EsmSpec,
22
+ type ModelState,
23
+ type WidgetModelId,
24
+ } from "./types";
25
+
26
+ /**
27
+ * Only edit sessions may swap widget code ("present" is an edit
28
+ * session with different chrome); a viewer's widget code is immutable.
29
+ */
30
+ function defaultIsEditMode(): boolean {
31
+ const mode = store.get(viewStateAtom).mode;
32
+ return mode === "edit" || mode === "present";
33
+ }
34
+
35
+ /**
36
+ * Maps model ids to stable runtimes and routes callers through their
37
+ * public behavior. Generation and view lifecycles stay inside each runtime.
38
+ */
39
+ export class WidgetRegistry implements WidgetResolver {
40
+ #runtimes = new Map<WidgetModelId, WidgetRuntime>();
41
+ #timeout: number;
42
+ #isEditMode: () => boolean;
43
+
44
+ constructor(timeout = 10_000, isEditMode: () => boolean = defaultIsEditMode) {
45
+ this.#timeout = timeout;
46
+ this.#isEditMode = isEditMode;
47
+ }
48
+
49
+ #getOrCreateRuntime(key: WidgetModelId): WidgetRuntime {
50
+ let runtime = this.#runtimes.get(key);
51
+ if (!runtime) {
52
+ const nextRuntime = new WidgetRuntime(key, {
53
+ timeout: this.#timeout,
54
+ isEditMode: this.#isEditMode,
55
+ createHost: (signal) => createHost(this, signal),
56
+ onModelTimeout: () => {
57
+ if (this.#runtimes.get(key) === nextRuntime) {
58
+ nextRuntime.dispose();
59
+ this.#runtimes.delete(key);
60
+ }
61
+ },
62
+ });
63
+ runtime = nextRuntime;
64
+ this.#runtimes.set(key, runtime);
65
+ }
66
+ return runtime;
67
+ }
68
+
69
+ /**
70
+ * Resolve the model for `key`, waiting for its `open` message if it
71
+ * hasn't arrived yet. Rejects after the registry timeout so a ref to
72
+ * a model that never opens fails loudly instead of hanging.
73
+ */
74
+ getModel(key: WidgetModelId): Promise<Model<ModelState>> {
75
+ return this.#getOrCreateRuntime(key).getModel();
76
+ }
77
+
78
+ /**
79
+ * Get a model synchronously if it exists and has been resolved.
80
+ * Returns undefined if the model doesn't exist or is still pending.
81
+ */
82
+ getModelSync(key: WidgetModelId): Model<ModelState> | undefined {
83
+ return this.#runtimes.get(key)?.getModelSync();
84
+ }
85
+
86
+ /**
87
+ * Create a model with a managed lifecycle signal.
88
+ * The signal is aborted when the entry is deleted.
89
+ */
90
+ createModel(
91
+ key: WidgetModelId,
92
+ factory: (signal: AbortSignal) => Model<ModelState>,
93
+ ): void {
94
+ this.#getOrCreateRuntime(key).createModel(factory);
95
+ }
96
+
97
+ setModel(key: WidgetModelId, model: Model<ModelState>): void {
98
+ this.#getOrCreateRuntime(key).setModel(model);
99
+ }
100
+
101
+ /**
102
+ * Record where this widget's code can be imported from and, in an
103
+ * edit session, hot-swap a live generation whose code changed.
104
+ *
105
+ * Specs only come from kernel-authored notifications; model state
106
+ * must never be treated as code (it is client-writable).
107
+ */
108
+ setSpec(key: WidgetModelId, spec: EsmSpec): void {
109
+ this.#getOrCreateRuntime(key).setSpec(spec);
110
+ }
111
+
112
+ getWidget<T = unknown>(key: WidgetModelId) {
113
+ return this.#getOrCreateRuntime(key).getWidget<T>();
114
+ }
115
+
116
+ createView(options: {
117
+ modelId: WidgetModelId;
118
+ el: HTMLElement;
119
+ signal: AbortSignal;
120
+ }): Promise<void> {
121
+ return this.#getOrCreateRuntime(options.modelId).createView(options);
122
+ }
123
+
124
+ /**
125
+ * Tear down everything known about `key`: the model's lifecycle
126
+ * signal and the current generation.
127
+ */
128
+ delete(key: WidgetModelId): void {
129
+ const runtime = this.#runtimes.get(key);
130
+ if (!runtime) {
131
+ return;
132
+ }
133
+ runtime.dispose();
134
+ this.#runtimes.delete(key);
135
+ }
136
+ }
137
+
138
+ export const WIDGET_REGISTRY = new WidgetRegistry();
139
+
140
+ // The legacy ipywidgets escape hatch (`model.widget_manager.get_model`)
141
+ // resolves through the registry; assigned here to keep Model free of
142
+ // registry knowledge.
143
+ Model._modelManager = WIDGET_REGISTRY;
144
+
145
+ /**
146
+ * Handle an incoming model lifecycle notification from the backend.
147
+ *
148
+ * Messages are dispatched by method type:
149
+ * - "open": Initialize a new model or update existing one with initial state
150
+ * - "update": Update model state with new values
151
+ * - "custom": Forward custom message to model listeners
152
+ * - "close": Remove the model and its binding from the registry
153
+ */
154
+ export async function handleWidgetMessage(
155
+ registry: WidgetRegistry,
156
+ notification: NotificationMessageData<"model-lifecycle">,
157
+ ): Promise<void> {
158
+ const modelId = notification.model_id;
159
+ if (!isWidgetModelId(modelId)) {
160
+ throw new Error(`[anywidget] Invalid model id: ${String(modelId)}`);
161
+ }
162
+ const msg = notification.message;
163
+
164
+ // Decode base64 buffers to DataViews (present in open/update/custom messages)
165
+ const base64Buffers: Base64String[] = "buffers" in msg ? msg.buffers : [];
166
+ const buffers = base64Buffers.map(base64ToDataView);
167
+
168
+ switch (msg.method) {
169
+ case "open": {
170
+ const { state, buffer_paths = [] } = msg;
171
+ // Record the spec before resolving the model so a waiting
172
+ // getWidget sees it.
173
+ if (msg.esm_spec) {
174
+ registry.setSpec(modelId, msg.esm_spec);
175
+ }
176
+ const stateWithBuffers = decodeFromWire({
177
+ state,
178
+ bufferPaths: buffer_paths,
179
+ buffers,
180
+ });
181
+
182
+ // Check if a model already exists (created by the plugin using model_id reference)
183
+ // If so, just update its state instead of creating a duplicate
184
+ const existingModel = registry.getModelSync(modelId);
185
+ if (existingModel) {
186
+ getMarimoInternal(existingModel).updateAndEmitDiffs(stateWithBuffers);
187
+ return;
188
+ }
189
+
190
+ registry.createModel(modelId, (signal) => {
191
+ // In static exports there is no kernel, so comm calls are no-ops.
192
+ const comm: MarimoComm<ModelState> = isStaticNotebook()
193
+ ? {
194
+ sendUpdate: async () => undefined,
195
+ sendCustomMessage: async () => undefined,
196
+ }
197
+ : {
198
+ async sendUpdate(changeData) {
199
+ if (signal.aborted) {
200
+ Logger.debug(
201
+ `[Model] sendUpdate suppressed for model=${modelId} (signal aborted)`,
202
+ );
203
+ return;
204
+ }
205
+ const { state, buffers, bufferPaths } =
206
+ serializeBuffersToBase64(changeData);
207
+ await getRequestClient().sendModelValue({
208
+ modelId,
209
+ message: { method: "update", state, bufferPaths },
210
+ buffers,
211
+ });
212
+ },
213
+ async sendCustomMessage(content, buffers) {
214
+ if (signal.aborted) {
215
+ Logger.debug(
216
+ `[Model] sendCustomMessage suppressed for model=${modelId} (signal aborted)`,
217
+ );
218
+ return;
219
+ }
220
+ await getRequestClient().sendModelValue({
221
+ modelId,
222
+ message: { method: "custom", content },
223
+ buffers: buffers.map(dataViewToBase64),
224
+ });
225
+ },
226
+ };
227
+
228
+ return new Model(stateWithBuffers, comm, signal);
229
+ });
230
+ return;
231
+ }
232
+
233
+ case "custom": {
234
+ const model = await registry.getModel(modelId);
235
+ // For custom messages, we need to reconstruct the custom-message shape
236
+ getMarimoInternal(model).emitCustomMessage(
237
+ { method: "custom", content: msg.content },
238
+ buffers,
239
+ );
240
+ return;
241
+ }
242
+
243
+ case "close":
244
+ registry.delete(modelId); // aborts the model's signal, destroys the binding
245
+ return;
246
+
247
+ case "update": {
248
+ const { state, buffer_paths = [] } = msg;
249
+ // A spec on an update is a hot reload (handled by setSpec).
250
+ if (msg.esm_spec) {
251
+ registry.setSpec(modelId, msg.esm_spec);
252
+ }
253
+ const stateWithBuffers = decodeFromWire({
254
+ state,
255
+ bufferPaths: buffer_paths,
256
+ buffers,
257
+ });
258
+ const model = await registry.getModel(modelId);
259
+ getMarimoInternal(model).updateAndEmitDiffs(stateWithBuffers);
260
+ return;
261
+ }
262
+
263
+ default:
264
+ assertNever(msg);
265
+ }
266
+ }
267
+
268
+ repl(WIDGET_REGISTRY, "WIDGET_REGISTRY");
@@ -0,0 +1,138 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { AnyWidget, Initialize, Render } from "@anywidget/types";
3
+ import { Logger } from "@/utils/Logger";
4
+ import { hasFunctionProperty, isRecord } from "@/utils/records";
5
+
6
+ function isRenderHook(value: unknown): value is Render {
7
+ return typeof value === "function";
8
+ }
9
+
10
+ function isInitializeHook(value: unknown): value is Initialize {
11
+ return typeof value === "function";
12
+ }
13
+
14
+ function isWidgetDefinition(
15
+ value: unknown,
16
+ ): value is { render?: Render; initialize?: Initialize } {
17
+ return (
18
+ isRecord(value) &&
19
+ (isRenderHook(value.render) || isInitializeHook(value.initialize))
20
+ );
21
+ }
22
+
23
+ function isAnyWidget(value: unknown): value is AnyWidget {
24
+ return typeof value === "function" || isWidgetDefinition(value);
25
+ }
26
+
27
+ export function isAnyWidgetModule(mod: unknown): mod is { default: AnyWidget } {
28
+ return isRecord(mod) && isAnyWidget(mod.default);
29
+ }
30
+
31
+ const warnedLegacyNamedExportUrls = new Set<string>();
32
+ // Cache the synthesized widget per module namespace so its identity stays
33
+ // stable across re-renders (like a default export), avoiding needless
34
+ // WidgetBinding re-initialization.
35
+ const legacyWidgetCache = new WeakMap<object, AnyWidget>();
36
+
37
+ /**
38
+ * Resolve the AnyWidget from a loaded module: prefer the AFM-spec default
39
+ * export, otherwise synthesize one from legacy named `render`/`initialize`
40
+ * exports. Returns null if neither is present.
41
+ */
42
+ export function resolveAnyWidget(
43
+ mod: unknown,
44
+ jsUrl: string,
45
+ ): AnyWidget | null {
46
+ if (isAnyWidgetModule(mod)) {
47
+ return mod.default;
48
+ }
49
+
50
+ if (!isRecord(mod)) {
51
+ return null;
52
+ }
53
+
54
+ // Only fall back to legacy (pre-AFM) named exports when there is no default
55
+ // export at all; a present-but-invalid default should surface an error
56
+ // rather than be masked.
57
+ if (mod.default !== undefined) {
58
+ return null;
59
+ }
60
+
61
+ const render = isRenderHook(mod.render) ? mod.render : undefined;
62
+ const initialize = isInitializeHook(mod.initialize)
63
+ ? mod.initialize
64
+ : undefined;
65
+ const hasNamedRender = render !== undefined;
66
+ const hasNamedInitialize = initialize !== undefined;
67
+ if (!hasNamedRender && !hasNamedInitialize) {
68
+ return null;
69
+ }
70
+
71
+ const cached = legacyWidgetCache.get(mod);
72
+ if (cached) {
73
+ return cached;
74
+ }
75
+
76
+ if (!warnedLegacyNamedExportUrls.has(jsUrl)) {
77
+ warnedLegacyNamedExportUrls.add(jsUrl);
78
+ Logger.warn(
79
+ `Anywidget module at ${jsUrl} uses deprecated top-level named ` +
80
+ "exports (`render`/`initialize`). Per the AFM spec, use a default " +
81
+ "export instead: `export default { render }`. " +
82
+ "See https://anywidget.dev/en/afm/",
83
+ );
84
+ }
85
+
86
+ const widget: AnyWidget = { render, initialize };
87
+ legacyWidgetCache.set(mod, widget);
88
+ return widget;
89
+ }
90
+
91
+ export function getInvalidAnyWidgetModuleError(
92
+ mod: unknown,
93
+ jsUrl: string,
94
+ ): Error {
95
+ const afmDocs = "https://anywidget.dev/en/afm/";
96
+ const hasNamedRender = isRecord(mod) && hasFunctionProperty(mod, "render");
97
+ const hasNamedInitialize =
98
+ isRecord(mod) && hasFunctionProperty(mod, "initialize");
99
+
100
+ if (isRecord(mod) && mod.default !== undefined) {
101
+ return new Error(
102
+ `Anywidget module at ${jsUrl} has an invalid default export. ` +
103
+ "Expected a factory function or an object with `render` or `initialize`. " +
104
+ `See ${afmDocs}`,
105
+ );
106
+ }
107
+
108
+ if (hasNamedRender || hasNamedInitialize) {
109
+ const namedExports = [
110
+ hasNamedRender ? "`render`" : null,
111
+ hasNamedInitialize ? "`initialize`" : null,
112
+ ]
113
+ .filter(Boolean)
114
+ .join(" and ");
115
+ const lifecycleHooks = [
116
+ hasNamedRender ? "render" : null,
117
+ hasNamedInitialize ? "initialize" : null,
118
+ ].filter((hook): hook is string => hook !== null);
119
+ const defaultExportExample = `export default { ${lifecycleHooks.join(", ")} }`;
120
+ const namedExportExample =
121
+ lifecycleHooks.length === 1
122
+ ? `export function ${lifecycleHooks[0]}`
123
+ : "named export function ...";
124
+ return new Error(
125
+ `Anywidget module at ${jsUrl} uses named exports (${namedExports}). ` +
126
+ "Per the AFM spec, use a default export instead: " +
127
+ `\`${defaultExportExample}\` (not \`${namedExportExample}\`). ` +
128
+ `See ${afmDocs}`,
129
+ );
130
+ }
131
+
132
+ return new Error(
133
+ `Anywidget module at ${jsUrl} is missing a default export. ` +
134
+ "Per the AFM spec, use `export default { render }` or " +
135
+ "`export default async () => ({ render })`. " +
136
+ `See ${afmDocs}`,
137
+ );
138
+ }