@marimo-team/islands 0.23.14-dev58 → 0.23.14-dev59

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 (29) hide show
  1. package/dist/{code-visibility-Con-26vn.js → code-visibility-ChrxMIRR.js} +28 -28
  2. package/dist/main.js +1920 -1619
  3. package/dist/{reveal-component-CfKy-mTN.js → reveal-component-BLijd8xt.js} +1 -1
  4. package/package.json +2 -2
  5. package/src/core/islands/bootstrap.ts +3 -3
  6. package/src/core/websocket/useMarimoKernelConnection.tsx +3 -3
  7. package/src/mount.tsx +3 -3
  8. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -305
  9. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +121 -229
  10. package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
  11. package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
  12. package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
  13. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
  14. package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
  15. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
  16. package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
  17. package/src/plugins/impl/anywidget/host.ts +54 -0
  18. package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
  19. package/src/plugins/impl/anywidget/model.ts +59 -239
  20. package/src/plugins/impl/anywidget/registry.ts +268 -0
  21. package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
  22. package/src/plugins/impl/anywidget/runtime.ts +408 -0
  23. package/src/plugins/impl/anywidget/types.ts +14 -0
  24. package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
  25. package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
  26. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
  27. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
  28. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
  29. package/src/plugins/impl/anywidget/schemas.ts +0 -32
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
9
9
  import { lt as kioskModeAtom, pt as outputIsStale } from "./html-to-image-hevd6UWC.js";
10
10
  import "./chunk-5FQGJX7Z-BbqSm5gU.js";
11
11
  import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
12
- import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelGroup, i as DEFAULT_DECK_TRANSITION, lt as PanelResizeHandle, on as EyeOff, s as SlideSidebar, sn as Expand, st as Panel, t as useNotebookCodeAvailable, un as Code } from "./code-visibility-Con-26vn.js";
12
+ import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelGroup, i as DEFAULT_DECK_TRANSITION, lt as PanelResizeHandle, on as EyeOff, s as SlideSidebar, sn as Expand, st as Panel, t as useNotebookCodeAvailable, un as Code } from "./code-visibility-ChrxMIRR.js";
13
13
  import { X as useDebouncedCallback } from "./input-DtsN7xm-.js";
14
14
  import "./toDate-CWNNlFEX.js";
15
15
  import "./react-dom-BTJzcVJ9.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.14-dev58",
3
+ "version": "0.23.14-dev59",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@ai-sdk/react": "^3.0.134",
25
- "@anywidget/types": "^0.2.0",
25
+ "@anywidget/types": "^0.4.0",
26
26
  "@codemirror/autocomplete": "^6.20.1",
27
27
  "@codemirror/commands": "^6.10.2",
28
28
  "@codemirror/lang-markdown": "^6.5.0",
@@ -5,8 +5,8 @@ import { ISLAND_CSS_CLASSES, ISLAND_TAG_NAMES } from "@/core/islands/constants";
5
5
  import { renderHTML } from "@/plugins/core/RenderHTML";
6
6
  import {
7
7
  handleWidgetMessage,
8
- MODEL_MANAGER,
9
- } from "@/plugins/impl/anywidget/model";
8
+ WIDGET_REGISTRY,
9
+ } from "@/plugins/impl/anywidget/registry";
10
10
  import { initializePlugins } from "@/plugins/plugins";
11
11
  import { Functions } from "@/utils/functions";
12
12
  import {
@@ -253,7 +253,7 @@ function handleMessage(
253
253
  return;
254
254
 
255
255
  case "model-lifecycle":
256
- handleWidgetMessage(MODEL_MANAGER, msg.data);
256
+ handleWidgetMessage(WIDGET_REGISTRY, msg.data);
257
257
  return;
258
258
 
259
259
  case "consumer-capabilities":
@@ -16,8 +16,8 @@ import { useConnectionTransport } from "@/core/websocket/useWebSocket";
16
16
  import { renderHTML } from "@/plugins/core/RenderHTML";
17
17
  import {
18
18
  handleWidgetMessage,
19
- MODEL_MANAGER,
20
- } from "@/plugins/impl/anywidget/model";
19
+ WIDGET_REGISTRY,
20
+ } from "@/plugins/impl/anywidget/registry";
21
21
  import { logNever } from "@/utils/assertNever";
22
22
  import { prettyError } from "@/utils/errors";
23
23
  import {
@@ -284,7 +284,7 @@ export function useMarimoKernelConnection(opts: {
284
284
  }
285
285
 
286
286
  case "model-lifecycle":
287
- handleWidgetMessage(MODEL_MANAGER, msg.data);
287
+ handleWidgetMessage(WIDGET_REGISTRY, msg.data);
288
288
  return;
289
289
 
290
290
  case "remove-ui-elements":
package/src/mount.tsx CHANGED
@@ -54,8 +54,8 @@ import { notebookFileStore } from "./core/wasm/store";
54
54
  import { WebSocketState } from "./core/websocket/types";
55
55
  import {
56
56
  handleWidgetMessage,
57
- MODEL_MANAGER,
58
- } from "./plugins/impl/anywidget/model";
57
+ WIDGET_REGISTRY,
58
+ } from "./plugins/impl/anywidget/registry";
59
59
  import { vegaLoader } from "./plugins/impl/vega/loader";
60
60
  import { initializePlugins } from "./plugins/plugins";
61
61
  import { ThemeProvider } from "./theme/ThemeProvider";
@@ -369,7 +369,7 @@ function hydrateStaticModels(): void {
369
369
  return;
370
370
  }
371
371
  for (const notification of notifications) {
372
- handleWidgetMessage(MODEL_MANAGER, notification);
372
+ handleWidgetMessage(WIDGET_REGISTRY, notification);
373
373
  }
374
374
  }
375
375
 
@@ -1,32 +1,21 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- /* oxlint-disable typescript/no-explicit-any */
3
2
 
4
- import type { AnyWidget } from "@anywidget/types";
5
- import { useEffect, useRef } from "react";
3
+ import { useEffect, useRef, useState } from "react";
6
4
  import { z } from "zod";
7
- import { useAsyncData } from "@/hooks/useAsyncData";
8
- import type { HTMLElementNotDerivedFromRef } from "@/hooks/useEventListener";
9
5
  import { createPlugin } from "@/plugins/core/builder";
10
6
  import type { IPluginProps } from "@/plugins/types";
11
- import { prettyError } from "@/utils/errors";
12
- import { Logger } from "@/utils/Logger";
13
- import { hasFunctionProperty, isRecord } from "@/utils/records";
14
7
  import { ErrorBanner } from "../common/error-banner";
15
- import { getMarimoInternal, MODEL_MANAGER, type Model } from "./model";
16
- import type { ModelState, WidgetModelId } from "./types";
17
- import { BINDING_MANAGER, WIDGET_DEF_REGISTRY } from "./widget-binding";
8
+ import { WIDGET_REGISTRY } from "./registry";
9
+ import { isWidgetModelId, type WidgetModelId } from "./types";
18
10
 
19
11
  /**
20
- * AnyWidget asset data
12
+ * The component's only data attribute; everything else arrives through
13
+ * the model's comm messages, keyed by this id.
21
14
  */
22
15
  interface Data {
23
- jsUrl: string;
24
- jsHash: string;
25
16
  modelId: WidgetModelId;
26
17
  }
27
18
 
28
- type AnyWidgetState = ModelState;
29
-
30
19
  /**
31
20
  * Value payload sent by the frontend on state updates.
32
21
  *
@@ -37,315 +26,54 @@ interface ModelIdRef {
37
26
  model_id?: WidgetModelId;
38
27
  }
39
28
 
40
- export function useAnyWidgetModule(opts: { jsUrl: string; jsHash: string }) {
41
- const { jsUrl, jsHash } = opts;
42
-
43
- // JS is an ESM file with a render function on it
44
- // export function render({ model, el }) {
45
- // ...
46
- const {
47
- data: jsModule,
48
- error,
49
- refetch,
50
- } = useAsyncData(async () => {
51
- return await WIDGET_DEF_REGISTRY.getModule(jsUrl, jsHash);
52
- // Re-render on jsHash change (which is a hash of the contents of the file)
53
- // instead of a jsUrl change because URLs may change without the contents
54
- // actually changing (and we don't want to re-render on every change).
55
- // If there is an error loading the URL (e.g. maybe an invalid or old URL),
56
- // we also want to re-render.
57
- }, [jsHash]);
58
-
59
- // If there is an error and the jsUrl has changed, we want to re-render
60
- // because the URL may have changed to a valid URL.
61
- const hasError = Boolean(error);
62
- useEffect(() => {
63
- if (hasError && jsUrl) {
64
- WIDGET_DEF_REGISTRY.invalidate(jsHash);
65
- refetch();
66
- }
67
- }, [hasError, jsUrl]);
68
-
69
- return {
70
- jsModule,
71
- error,
72
- };
73
- }
74
-
75
- export function useMountCss(css: string | null | undefined, host: HTMLElement) {
76
- // Mount the CSS
77
- useEffect(() => {
78
- const shadowRoot = host.shadowRoot;
79
- if (!css || !shadowRoot) {
80
- return;
81
- }
82
-
83
- // Try constructed stylesheets first
84
- if (
85
- "adoptedStyleSheets" in Document.prototype &&
86
- "replace" in CSSStyleSheet.prototype
87
- ) {
88
- const sheet = new CSSStyleSheet();
89
- try {
90
- sheet.replaceSync(css);
91
- if (shadowRoot) {
92
- shadowRoot.adoptedStyleSheets = [
93
- ...shadowRoot.adoptedStyleSheets,
94
- sheet,
95
- ];
96
- }
97
- return () => {
98
- if (shadowRoot) {
99
- shadowRoot.adoptedStyleSheets =
100
- shadowRoot.adoptedStyleSheets.filter((s) => s !== sheet);
101
- }
102
- };
103
- } catch {
104
- // Fall through to inline styles if constructed sheets fail
105
- }
106
- }
107
-
108
- // Fallback to inline styles
109
- const style = document.createElement("style");
110
- style.innerHTML = css;
111
- shadowRoot.append(style);
112
- return () => {
113
- style.remove();
114
- };
115
- }, [css, host]);
116
- }
117
-
118
29
  export const AnyWidgetPlugin = createPlugin<ModelIdRef>("marimo-anywidget")
119
30
  .withData(
120
31
  z.object({
121
- jsUrl: z.string(),
122
- jsHash: z.string(),
123
- modelId: z.string().transform((v) => v as WidgetModelId),
32
+ modelId: z.custom<WidgetModelId>(isWidgetModelId, {
33
+ message: "Expected a non-empty widget model id",
34
+ }),
124
35
  }),
125
36
  )
126
37
  .withFunctions({})
127
38
  .renderer((props) => <AnyWidgetSlot {...props} />);
128
39
 
129
- const AnyWidgetSlot = (props: IPluginProps<ModelIdRef, Data>) => {
130
- const { jsUrl, jsHash, modelId } = props.data;
131
- const host = props.host as HTMLElementNotDerivedFromRef;
132
-
133
- const { jsModule, error } = useAnyWidgetModule({ jsUrl, jsHash });
134
-
135
- if (error) {
136
- return <ErrorBanner error={error} />;
137
- }
138
-
139
- if (!jsModule) {
140
- return null;
141
- }
142
-
143
- const widget = resolveAnyWidget(jsModule, jsUrl);
144
- if (!widget) {
145
- return (
146
- <ErrorBanner error={getInvalidAnyWidgetModuleError(jsModule, jsUrl)} />
147
- );
148
- }
149
-
150
- return (
151
- <LoadedSlot
152
- // Force remount when the widget module or model changes (cell re-run).
153
- key={`${jsHash}:${modelId}`}
154
- widget={widget}
155
- modelId={modelId}
156
- host={host}
157
- />
158
- );
159
- };
160
-
161
- /**
162
- * Run the anywidget module
163
- *
164
- * Per AFM spec (anywidget.dev/en/afm):
165
- * - initialize() is called once per model lifetime
166
- * - render() is called once per view (can be multiple per model)
167
- */
168
- async function runAnyWidgetModule<T extends AnyWidgetState>(
169
- widgetDef: AnyWidget<T>,
170
- model: Model<T>,
171
- modelId: WidgetModelId,
172
- el: HTMLElement,
173
- signal: AbortSignal,
174
- ): Promise<void> {
175
- // Clear the element, in case the widget is re-rendering
176
- el.innerHTML = "";
177
-
178
- try {
179
- const binding = BINDING_MANAGER.getOrCreate(modelId);
180
- const render = await binding.bind(widgetDef, model);
181
- await render(el, signal);
182
- // Replay current model values so render listeners observe hydrated state
183
- // even if backend updates arrived before listeners were attached.
184
- getMarimoInternal(model).reemitState();
185
- } catch (error) {
186
- Logger.error("Error rendering anywidget", error);
187
- el.classList.add("text-error");
188
- el.innerHTML = `Error rendering anywidget: ${prettyError(error)}`;
189
- }
190
- }
191
-
192
- function isAnyWidgetModule(mod: any): mod is { default: AnyWidget } {
193
- if (!mod.default) {
194
- return false;
195
- }
196
-
197
- return (
198
- typeof mod.default === "function" ||
199
- typeof mod.default?.render === "function" ||
200
- typeof mod.default?.initialize === "function"
201
- );
202
- }
203
-
204
- const warnedLegacyNamedExportUrls = new Set<string>();
205
- // Cache the synthesized widget per module namespace so its identity stays
206
- // stable across re-renders (like a default export), avoiding needless
207
- // WidgetBinding re-initialization.
208
- const legacyWidgetCache = new WeakMap<object, AnyWidget>();
209
-
210
40
  /**
211
- * Resolve the AnyWidget from a loaded module: prefer the AFM-spec default
212
- * export, otherwise synthesize one from legacy named `render`/`initialize`
213
- * exports. Returns null if neither is present.
41
+ * React adapter for a runtime-owned view. React supplies only the render
42
+ * target and its lifetime; the runtime owns models, generations, CSS,
43
+ * composition, and hot reload.
214
44
  */
215
- function resolveAnyWidget(mod: any, jsUrl: string): AnyWidget | null {
216
- if (isAnyWidgetModule(mod)) {
217
- return mod.default;
218
- }
219
-
220
- // Only fall back to legacy (pre-AFM) named exports when there is no default
221
- // export at all; a present-but-invalid default should surface an error
222
- // rather than be masked.
223
- if (mod?.default != null) {
224
- return null;
225
- }
226
-
227
- const hasNamedRender = typeof mod?.render === "function";
228
- const hasNamedInitialize = typeof mod?.initialize === "function";
229
- if (!hasNamedRender && !hasNamedInitialize) {
230
- return null;
231
- }
232
-
233
- const cached = legacyWidgetCache.get(mod);
234
- if (cached) {
235
- return cached;
236
- }
237
-
238
- if (!warnedLegacyNamedExportUrls.has(jsUrl)) {
239
- warnedLegacyNamedExportUrls.add(jsUrl);
240
- Logger.warn(
241
- `Anywidget module at ${jsUrl} uses deprecated top-level named ` +
242
- "exports (`render`/`initialize`). Per the AFM spec, use a default " +
243
- "export instead: `export default { render }`. " +
244
- "See https://anywidget.dev/en/afm/",
245
- );
246
- }
247
-
248
- const widget: AnyWidget = { render: mod.render, initialize: mod.initialize };
249
- legacyWidgetCache.set(mod, widget);
250
- return widget;
251
- }
252
-
253
- function getInvalidAnyWidgetModuleError(mod: unknown, jsUrl: string): Error {
254
- const afmDocs = "https://anywidget.dev/en/afm/";
255
- const hasNamedRender = isRecord(mod) && hasFunctionProperty(mod, "render");
256
- const hasNamedInitialize =
257
- isRecord(mod) && hasFunctionProperty(mod, "initialize");
258
-
259
- if (hasNamedRender || hasNamedInitialize) {
260
- const namedExports = [
261
- hasNamedRender ? "`render`" : null,
262
- hasNamedInitialize ? "`initialize`" : null,
263
- ]
264
- .filter(Boolean)
265
- .join(" and ");
266
- const lifecycleHooks = [
267
- hasNamedRender ? "render" : null,
268
- hasNamedInitialize ? "initialize" : null,
269
- ].filter((hook): hook is string => hook !== null);
270
- const defaultExportExample = `export default { ${lifecycleHooks.join(", ")} }`;
271
- const namedExportExample =
272
- lifecycleHooks.length === 1
273
- ? `export function ${lifecycleHooks[0]}`
274
- : "named export function ...";
275
- return new Error(
276
- `Anywidget module at ${jsUrl} uses named exports (${namedExports}). ` +
277
- "Per the AFM spec, use a default export instead: " +
278
- `\`${defaultExportExample}\` (not \`${namedExportExample}\`). ` +
279
- `See ${afmDocs}`,
280
- );
281
- }
282
-
283
- if (!isRecord(mod) || mod.default === undefined) {
284
- return new Error(
285
- `Anywidget module at ${jsUrl} is missing a default export. ` +
286
- "Per the AFM spec, use `export default { render }` or " +
287
- "`export default async () => ({ render })`. " +
288
- `See ${afmDocs}`,
289
- );
290
- }
291
-
292
- return new Error(
293
- `Anywidget module at ${jsUrl} has an invalid default export. ` +
294
- "Expected a factory function or an object with `render` or `initialize`. " +
295
- `See ${afmDocs}`,
296
- );
297
- }
298
-
299
- interface Props<T extends AnyWidgetState> {
300
- widget: AnyWidget<T>;
301
- modelId: WidgetModelId;
302
- host: HTMLElementNotDerivedFromRef;
303
- }
304
-
305
- const LoadedSlot = <T extends AnyWidgetState>({
306
- widget,
307
- modelId,
308
- host,
309
- }: Props<T> & { widget: AnyWidget<T> }) => {
45
+ const AnyWidgetSlot = (props: IPluginProps<ModelIdRef, Data>) => {
46
+ const { modelId } = props.data;
310
47
  const htmlRef = useRef<HTMLDivElement>(null);
311
-
312
- // value is already decoded from wire format, may be null if waiting for open message
313
- const model = MODEL_MANAGER.getSync(modelId);
314
-
315
- if (!model) {
316
- Logger.error("Model not found for modelId", modelId);
317
- }
318
-
319
- const css = model?.get("_css");
320
- useMountCss(css, host);
48
+ const [error, setError] = useState<Error>();
321
49
 
322
50
  useEffect(() => {
323
- if (!htmlRef.current || !model) {
51
+ const el = htmlRef.current;
52
+ if (!el) {
324
53
  return;
325
54
  }
326
55
  const controller = new AbortController();
327
- runAnyWidgetModule(
328
- widget,
329
- model,
56
+ setError(undefined);
57
+ WIDGET_REGISTRY.createView({
330
58
  modelId,
331
- htmlRef.current,
332
- controller.signal,
333
- );
59
+ el,
60
+ signal: controller.signal,
61
+ }).catch((error) => {
62
+ if (!controller.signal.aborted) {
63
+ setError(error instanceof Error ? error : new Error(String(error)));
64
+ }
65
+ });
334
66
  return () => controller.abort();
335
- // We re-run the widget when the modelId changes, which means the cell
336
- // that created the Widget has been re-run.
337
- // We need to re-run the widget because it may contain initialization code
338
- // that could be reset by the new widget.
339
- // See example: https://github.com/marimo-team/marimo/issues/3962#issuecomment-2703184123
340
- }, [widget, modelId, model]);
67
+ }, [modelId]);
341
68
 
342
- return <div ref={htmlRef} />;
69
+ return (
70
+ <>
71
+ {error ? <ErrorBanner error={error} /> : null}
72
+ <div ref={htmlRef} />
73
+ </>
74
+ );
343
75
  };
344
76
 
345
77
  export const visibleForTesting = {
346
- LoadedSlot,
347
- runAnyWidgetModule,
348
- isAnyWidgetModule,
349
- resolveAnyWidget,
350
- getInvalidAnyWidgetModuleError,
78
+ AnyWidgetSlot,
351
79
  };