Skip to content

Commit 5bba63f

Browse files
authored
Merge branch 'marimo-team:main' into main
2 parents 3c37046 + e58e848 commit 5bba63f

File tree

23 files changed

+1462
-344
lines changed

23 files changed

+1462
-344
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: 🏷️ Enforce Pull Request Label
2+
3+
# Enforces that at least one label is selected on pull requests
4+
5+
on:
6+
pull_request:
7+
types: [labeled, unlabeled, opened, reopened, edited, synchronize]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
enforce-label:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: yogevbd/enforce-label-action@2.2.2
17+
with:
18+
REQUIRED_LABELS_ANY: "enhancement,bug,documentation,internal,preview,dependencies,other"
19+
REQUIRED_LABELS_ANY_DESCRIPTION: "🏷️ Select at least one label: ['enhancement','bug','documentation','internal','preview','dependencies','other']"

.github/workflows/labeler.yml

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,19 @@
11
name: 🏷️ Pull Request Labeler
22

3-
# Label pull requests based on config in .github/labeler.yml
4-
# And enforces that at least one label is selected
3+
# Auto-label pull requests based on config in .github/labeler.yml
54

65
on:
76
pull_request_target:
87
types: [labeled, unlabeled, opened, reopened, edited, synchronize]
9-
pull_request:
10-
types: [labeled, unlabeled, opened, reopened, edited, synchronize]
11-
12-
concurrency:
13-
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
14-
cancel-in-progress: true
158

169
permissions:
1710
contents: read
1811
pull-requests: write
1912

2013
jobs:
2114
auto-label:
22-
if: github.event_name == 'pull_request_target'
2315
runs-on: ubuntu-latest
2416
steps:
2517
- uses: actions/labeler@v5
2618
with:
2719
repo-token: ${{ secrets.GITHUB_TOKEN }}
28-
29-
enforce-label:
30-
if: github.event_name == 'pull_request'
31-
runs-on: ubuntu-latest
32-
steps:
33-
- uses: yogevbd/enforce-label-action@2.2.2
34-
with:
35-
REQUIRED_LABELS_ANY: "enhancement,bug,documentation,internal,preview,dependencies,other"
36-
REQUIRED_LABELS_ANY_DESCRIPTION: "🏷️ Select at least one label: ['enhancement','bug','documentation','internal','preview','dependencies','other']"

SECURITY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ We would like to acknowledge and thank the following individuals for their respo
4343
- @acepace
4444
- @devgi
4545
- @W-M-T (Ward Theunisse)
46+
- @doredry
4647

4748
Your contributions help keep marimo safe for the entire community. We encourage security researchers to report issues and welcome your help in making marimo more secure.

frontend/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,16 @@
44
import type { AnyWidget } from "@anywidget/types";
55
import { useEffect, useRef } from "react";
66
import { z } from "zod";
7-
import { asRemoteURL } from "@/core/runtime/config";
8-
import { resolveVirtualFileURL } from "@/core/static/files";
9-
import { isStaticNotebook } from "@/core/static/static-state";
107
import { useAsyncData } from "@/hooks/useAsyncData";
118
import type { HTMLElementNotDerivedFromRef } from "@/hooks/useEventListener";
129
import { createPlugin } from "@/plugins/core/builder";
1310
import type { IPluginProps } from "@/plugins/types";
1411
import { prettyError } from "@/utils/errors";
1512
import { Logger } from "@/utils/Logger";
1613
import { ErrorBanner } from "../common/error-banner";
17-
import { getMarimoInternal, MODEL_MANAGER, type Model } from "./model";
14+
import { MODEL_MANAGER, type Model } from "./model";
1815
import type { ModelState, WidgetModelId } from "./types";
16+
import { BINDING_MANAGER, WIDGET_DEF_REGISTRY } from "./widget-binding";
1917

2018
/**
2119
* AnyWidget asset data
@@ -48,12 +46,7 @@ export function useAnyWidgetModule(opts: { jsUrl: string; jsHash: string }) {
4846
error,
4947
refetch,
5048
} = useAsyncData(async () => {
51-
let url = asRemoteURL(jsUrl).toString();
52-
// In static notebooks, resolve virtual files to blob URLs for import()
53-
if (isStaticNotebook()) {
54-
url = resolveVirtualFileURL(url);
55-
}
56-
return await import(/* @vite-ignore */ url);
49+
return await WIDGET_DEF_REGISTRY.getModule(jsUrl, jsHash);
5750
// Re-render on jsHash change (which is a hash of the contents of the file)
5851
// instead of a jsUrl change because URLs may change without the contents
5952
// actually changing (and we don't want to re-render on every change).
@@ -66,6 +59,7 @@ export function useAnyWidgetModule(opts: { jsUrl: string; jsHash: string }) {
6659
const hasError = Boolean(error);
6760
useEffect(() => {
6861
if (hasError && jsUrl) {
62+
WIDGET_DEF_REGISTRY.invalidate(jsHash);
6963
refetch();
7064
}
7165
}, [hasError, jsUrl]);
@@ -180,14 +174,16 @@ const AnyWidgetSlot = (props: IPluginProps<ModelIdRef, Data>) => {
180174
async function runAnyWidgetModule<T extends AnyWidgetState>(
181175
widgetDef: AnyWidget<T>,
182176
model: Model<T>,
177+
modelId: WidgetModelId,
183178
el: HTMLElement,
184179
signal: AbortSignal,
185180
): Promise<void> {
186181
// Clear the element, in case the widget is re-rendering
187182
el.innerHTML = "";
188183

189184
try {
190-
const render = await getMarimoInternal(model).resolveWidget(widgetDef);
185+
const binding = BINDING_MANAGER.getOrCreate(modelId);
186+
const render = await binding.bind(widgetDef, model);
191187
await render(el, signal);
192188
} catch (error) {
193189
Logger.error("Error rendering anywidget", error);
@@ -231,7 +227,13 @@ const LoadedSlot = <T extends AnyWidgetState>({
231227
return;
232228
}
233229
const controller = new AbortController();
234-
runAnyWidgetModule(widget, model, htmlRef.current, controller.signal);
230+
runAnyWidgetModule(
231+
widget,
232+
model,
233+
modelId,
234+
htmlRef.current,
235+
controller.signal,
236+
);
235237
return () => controller.abort();
236238
// We re-run the widget when the modelId changes, which means the cell
237239
// that created the Widget has been re-run.

frontend/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/* Copyright 2026 Marimo. All rights reserved. */
22

33
import { render, waitFor } from "@testing-library/react";
4-
import { beforeEach, describe, expect, it, vi } from "vitest";
4+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
55
import { TestUtils } from "@/__tests__/test-helpers";
66
import type { HTMLElementNotDerivedFromRef } from "@/hooks/useEventListener";
77
import { visibleForTesting } from "../AnyWidgetPlugin";
88
import { MODEL_MANAGER, Model } from "../model";
99
import type { WidgetModelId } from "../types";
10+
import { BINDING_MANAGER } from "../widget-binding";
1011

1112
const { LoadedSlot } = visibleForTesting;
1213

@@ -19,14 +20,6 @@ const mockWidget = {
1920
render: vi.fn(),
2021
};
2122

22-
vi.mock("../AnyWidgetPlugin", async () => {
23-
const originalModule = await vi.importActual("../AnyWidgetPlugin");
24-
return {
25-
...originalModule,
26-
runAnyWidgetModule: vi.fn(),
27-
};
28-
});
29-
3023
describe("LoadedSlot", () => {
3124
const modelId = asModelId("test-model-id");
3225
let mockModel: Model<{ count: number }>;
@@ -56,6 +49,10 @@ describe("LoadedSlot", () => {
5649
MODEL_MANAGER.set(modelId, mockModel);
5750
});
5851

52+
afterEach(() => {
53+
BINDING_MANAGER.destroy(modelId);
54+
});
55+
5956
it("should render a div with ref", () => {
6057
const { container } = render(<LoadedSlot {...mockProps} />);
6158
expect(container.querySelector("div")).not.toBeNull();

frontend/src/plugins/impl/anywidget/__tests__/model.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
visibleForTesting,
1717
} from "../model";
1818
import type { WidgetModelId } from "../types";
19+
import { BINDING_MANAGER } from "../widget-binding";
1920

2021
const { ModelManager } = visibleForTesting;
2122

@@ -376,4 +377,20 @@ describe("ModelManager", () => {
376377
});
377378
await expect(modelManager.get(testId)).rejects.toThrow();
378379
});
380+
381+
it("should destroy binding on close message", async () => {
382+
const model = new Model({ count: 0 }, createMockComm());
383+
modelManager.set(testId, model);
384+
385+
// Create a binding for this model
386+
BINDING_MANAGER.getOrCreate(testId);
387+
expect(BINDING_MANAGER.has(testId)).toBe(true);
388+
389+
await handleWidgetMessage(modelManager, {
390+
model_id: testId,
391+
message: { method: "close" },
392+
});
393+
394+
expect(BINDING_MANAGER.has(testId)).toBe(false);
395+
});
379396
});

0 commit comments

Comments
 (0)