@marimo-team/islands 0.23.14-dev9 → 0.23.15-dev0
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.
- package/dist/{ConnectedDataExplorerComponent-DXBx_nQg.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
- package/dist/{chat-ui-DYBcNEdd.js → chat-ui-DinOvbWu.js} +3256 -3052
- package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
- package/dist/{code-visibility-BZArWgy6.js → code-visibility-DDwWvOQa.js} +905 -889
- package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
- package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
- package/dist/{formats-WsOgyW_K.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-qpmKyAPn.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
- package/dist/{html-to-image-MqcD07Bw.js → html-to-image-_wGfk8V-.js} +2389 -2323
- package/dist/{input-BSdZp5Ng.js → input-DtsN7xm-.js} +1 -1
- package/dist/main.js +3667 -1838
- package/dist/{mermaid-D-HYBMEV.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
- package/dist/{process-output-Dt3icftd.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-CKomb1B5.js → reveal-component-CHu9H-QX.js} +600 -596
- package/dist/{spec-CnTgI25l.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-D1Z7ZXWh.js → toDate-CWNNlFEX.js} +15 -7
- package/dist/{useAsyncData-BMc8itk2.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-ZwmDBRDY.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-CxffarYV.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-yGsGEk82.js → useTheme-6eZ3GOTS.js} +5 -2
- package/dist/{vega-component-BFJTyykA.js → vega-component-DzyyM9fc.js} +5 -5
- package/package.json +4 -4
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/CellStatus.test.tsx +1 -2
- package/src/components/app-config/user-config-form.tsx +52 -0
- package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
- package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
- package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
- package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
- package/src/components/data-table/charts/charts.tsx +23 -1
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
- package/src/components/debugger/debugger-code.tsx +7 -2
- package/src/components/editor/cell/CellStatus.tsx +1 -20
- package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
- package/src/components/editor/cell/cell-context-menu.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
- package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
- package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
- package/src/components/editor/code/readonly-python-code.tsx +60 -33
- package/src/components/editor/errors/__tests__/auto-fix.test.ts +11 -107
- package/src/components/editor/errors/auto-fix.tsx +11 -31
- package/src/components/editor/notebook-cell.tsx +7 -0
- package/src/components/editor/output/ImageOutput.tsx +20 -6
- package/src/components/editor/output/MarimoTracebackOutput.tsx +20 -3
- package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
- package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
- package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
- package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
- package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
- package/src/components/slides/__tests__/slide.test.tsx +33 -0
- package/src/components/slides/minimap.tsx +7 -1
- package/src/components/slides/reveal-component.tsx +3 -0
- package/src/components/slides/slide-cell-view.tsx +10 -6
- package/src/components/slides/slide.tsx +16 -13
- package/src/components/tracing/tracing.tsx +2 -1
- package/src/core/ai/context/__tests__/registry.test.ts +133 -3
- package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
- package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
- package/src/core/ai/context/providers/datasource.ts +27 -2
- package/src/core/ai/context/providers/error.ts +226 -36
- package/src/core/ai/context/registry.ts +77 -43
- package/src/core/cells/__tests__/cell.test.ts +39 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/cell.ts +5 -3
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/core/codemirror/__tests__/setup.test.ts +33 -1
- package/src/core/codemirror/ai/resources.ts +15 -10
- package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
- package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
- package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
- package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
- package/src/core/codemirror/cells/debugger-state.ts +133 -0
- package/src/core/codemirror/cells/extensions.ts +32 -4
- package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
- package/src/core/codemirror/go-to-definition/extension.ts +3 -3
- package/src/core/codemirror/go-to-definition/underline.ts +5 -14
- package/src/core/codemirror/go-to-definition/utils.ts +49 -1
- package/src/core/config/__tests__/config-schema.test.ts +17 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +10 -0
- package/src/core/config/feature-flag.tsx +4 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/islands/bootstrap.ts +4 -3
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
- package/src/core/lifecycle/render-policy.ts +125 -0
- package/src/core/mime.ts +11 -4
- package/src/core/network/__tests__/api.test.ts +17 -0
- package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
- package/src/core/network/api.ts +17 -6
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +10 -1
- package/src/core/network/types.ts +2 -0
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/__tests__/runtime.test.ts +45 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/runtime/runtime.ts +30 -16
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +13 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
- package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
- package/src/core/websocket/transports/sse.ts +308 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
- package/src/core/websocket/useWebSocket.tsx +12 -2
- package/src/css/app/codemirror.css +14 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/mount.tsx +3 -3
- package/src/plugins/core/RenderHTML.tsx +35 -1
- package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
- package/src/plugins/impl/DataTablePlugin.tsx +1 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
- package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
- package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
- package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
- package/src/plugins/impl/anywidget/host.ts +54 -0
- package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
- package/src/plugins/impl/anywidget/model.ts +59 -239
- package/src/plugins/impl/anywidget/registry.ts +268 -0
- package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
- package/src/plugins/impl/anywidget/runtime.ts +422 -0
- package/src/plugins/impl/anywidget/types.ts +14 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
- package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
- package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
- package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/time.ts +20 -0
- package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
- package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
- package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
- package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
- package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
- package/src/core/wasm/__tests__/state.test.ts +0 -124
- 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
|
+
}
|