@marimo-team/islands 0.25.3-dev232 → 0.25.3-dev233

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.
@@ -1,6 +1,6 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { t } from "./react-sHtZdjeP.js";
3
- import { a as n } from "./glide-data-editor-DpC5t4zI.js";
3
+ import { a as n } from "./glide-data-editor-DyC7T9ey.js";
4
4
  //#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.3.0_27cffe7f1bc30287d59ac27db3240c5b/node_modules/@glideapps/glide-data-grid/dist/esm/internal/data-grid-overlay-editor/private/number-overlay-editor-style.js
5
5
  var r = /* @__PURE__ */ e(t()), i = /*#__PURE__*/ n("div")({
6
6
  name: "NumberOverlayEditorStyle",
@@ -1,4 +1,4 @@
1
- import { k as e } from "./html-to-image-0FooaKop.js";
1
+ import { k as e } from "./html-to-image-Gx4q3ZoD.js";
2
2
  import { m as t } from "./tooltip-C3UnvsRe.js";
3
3
  import { $ as n, et as r } from "./error-entries-CRPfxMqm.js";
4
4
  import { t as i } from "./strings-BGW-fXVJ.js";
@@ -3,7 +3,7 @@ import { c as t, g as n, h as r, m as i, t as a } from "./button-B6RjDKJx.js";
3
3
  import { t as o } from "./react-sHtZdjeP.js";
4
4
  import { t as s } from "./compiler-runtime-CldRdSsv.js";
5
5
  import { m as c, t as l } from "./tooltip-C3UnvsRe.js";
6
- import { $ as u, Q as d, Z as f, an as p, i as m, l as h, ln as g, n as _, on as v, r as y, t as b, u as x } from "./common-B85jDHUj.js";
6
+ import { $ as u, Q as d, Z as f, an as p, i as m, l as h, ln as g, n as _, on as v, r as y, t as b, u as x } from "./common-D4YIvHFJ.js";
7
7
  import { M as S } from "./select-BSxlITBo.js";
8
8
  import { Y as ee } from "./dropdown-menu-BRiE7Vnp.js";
9
9
  import { Pt as C } from "./error-entries-CRPfxMqm.js";
@@ -11,7 +11,7 @@ import { t as w } from "./jsx-runtime-3BD1kT0s.js";
11
11
  import { r as T } from "./use-restore-focus-BWO0z1lo.js";
12
12
  import { D as E, t as D, x as O } from "./utils-DXkj82sK.js";
13
13
  import { o as k } from "./static-state-BldGd99T.js";
14
- import { a as A, n as te, t as ne } from "./code-visibility-BFAMp8wF.js";
14
+ import { a as A, n as te, t as ne } from "./code-visibility-Dqc_vlop.js";
15
15
  /* empty css */
16
16
  var j = c("sticky-note", [["path", {
17
17
  d: "M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.3-dev232",
3
+ "version": "0.25.3-dev233",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -349,7 +349,7 @@ export function useNotebookActions({
349
349
  code: code.contents,
350
350
  baseUrl: `${Constants.molab}/new`,
351
351
  });
352
- window.open(url, "_blank");
352
+ window.open(url, "_blank", "noopener");
353
353
  },
354
354
  },
355
355
  ],
@@ -662,35 +662,35 @@ export function useNotebookActions({
662
662
  icon: <BookMarkedIcon size={14} strokeWidth={1.5} />,
663
663
  label: "Documentation",
664
664
  handle: () => {
665
- window.open(Constants.docsPage, "_blank");
665
+ window.open(Constants.docsPage, "_blank", "noopener");
666
666
  },
667
667
  },
668
668
  {
669
669
  icon: <GitHubIcon className="h-3.5 w-3.5" />,
670
670
  label: "GitHub",
671
671
  handle: () => {
672
- window.open(Constants.githubPage, "_blank");
672
+ window.open(Constants.githubPage, "_blank", "noopener");
673
673
  },
674
674
  },
675
675
  {
676
676
  icon: <MessagesSquareIcon size={14} strokeWidth={1.5} />,
677
677
  label: "Discord Community",
678
678
  handle: () => {
679
- window.open(Constants.discordLink, "_blank");
679
+ window.open(Constants.discordLink, "_blank", "noopener");
680
680
  },
681
681
  },
682
682
  {
683
683
  icon: <YouTubeIcon className="h-3.5 w-3.5" />,
684
684
  label: "YouTube",
685
685
  handle: () => {
686
- window.open(Constants.youtube, "_blank");
686
+ window.open(Constants.youtube, "_blank", "noopener");
687
687
  },
688
688
  },
689
689
  {
690
690
  icon: <FileTextIcon size={14} strokeWidth={1.5} />,
691
691
  label: "Changelog",
692
692
  handle: () => {
693
- window.open(Constants.releasesPage, "_blank");
693
+ window.open(Constants.releasesPage, "_blank", "noopener");
694
694
  },
695
695
  },
696
696
  ],
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import type { JSX } from "react";
3
+ import { useSanitizeHtml } from "@/plugins/core/sanitize";
3
4
 
4
5
  interface Props {
5
6
  src: string;
@@ -7,5 +8,10 @@ interface Props {
7
8
  }
8
9
 
9
10
  export const VideoOutput = ({ src, className }: Props): JSX.Element => {
11
+ const shouldSanitize = useSanitizeHtml();
12
+ if (shouldSanitize) {
13
+ return <video className={className} src={src} controls={true} />;
14
+ }
15
+ // oxlint-disable-next-line react/iframe-missing-sandbox, react-doctor/iframe-missing-sandbox -- Trusted notebook output can include active documents.
10
16
  return <iframe className={className} src={src} title="Video output" />;
11
17
  };
@@ -0,0 +1,131 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { act, render } from "@testing-library/react";
3
+ import { Provider } from "jotai";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
6
+ import { OutputRenderer } from "@/components/editor/Output";
7
+ import { configOverridesAtom } from "@/core/config/config";
8
+ import { initialModeAtom } from "@/core/mode";
9
+ import { store } from "@/core/state/jotai";
10
+ import { VideoOutput } from "../VideoOutput";
11
+
12
+ function snapshotTrustState() {
13
+ return {
14
+ hasRunAnyCell: store.get(hasRunAnyCellAtom),
15
+ overrides: store.get(configOverridesAtom),
16
+ mode: store.get(initialModeAtom),
17
+ exportContext: window.__MARIMO_EXPORT_CONTEXT__,
18
+ };
19
+ }
20
+
21
+ describe("VideoOutput", () => {
22
+ let previous: ReturnType<typeof snapshotTrustState>;
23
+
24
+ beforeEach(() => {
25
+ previous = snapshotTrustState();
26
+ store.set(hasRunAnyCellAtom, false);
27
+ store.set(configOverridesAtom, { runtime: { auto_instantiate: false } });
28
+ store.set(initialModeAtom, "edit");
29
+ delete window.__MARIMO_EXPORT_CONTEXT__;
30
+ });
31
+
32
+ afterEach(() => {
33
+ store.set(hasRunAnyCellAtom, previous.hasRunAnyCell);
34
+ store.set(configOverridesAtom, previous.overrides);
35
+ store.set(initialModeAtom, previous.mode);
36
+ if (previous.exportContext) {
37
+ window.__MARIMO_EXPORT_CONTEXT__ = previous.exportContext;
38
+ } else {
39
+ delete window.__MARIMO_EXPORT_CONTEXT__;
40
+ }
41
+ });
42
+
43
+ function renderVideo(src = "https://example.com/video.mp4") {
44
+ return render(
45
+ <Provider store={store}>
46
+ <VideoOutput src={src} className="output" />
47
+ </Provider>,
48
+ );
49
+ }
50
+
51
+ it.each([
52
+ "https://example.com/video.mp4",
53
+ "data:video/mp4;base64,AAAA",
54
+ "./@file/123-video.mp4",
55
+ ])("preserves the media source and controls before execution: %s", (src) => {
56
+ const { container } = renderVideo(src);
57
+ const video = container.querySelector("video");
58
+ expect(video).toHaveAttribute("src", src);
59
+ expect(video).toHaveAttribute("controls");
60
+ expect(video).toHaveClass("output");
61
+ expect(container.querySelector("iframe")).toBeNull();
62
+ });
63
+
64
+ it("keeps direct and JSON video outputs out of frames before execution", () => {
65
+ const src = `data:text/html,${encodeURIComponent("<script>parent.postMessage('cached-video', '*')</script>")}`;
66
+ const { container } = render(
67
+ <Provider store={store}>
68
+ <OutputRenderer
69
+ message={{ channel: "output", mimetype: "video/mp4", data: src }}
70
+ />
71
+ <OutputRenderer
72
+ message={{
73
+ channel: "output",
74
+ mimetype: "application/json",
75
+ data: { video: `video/mp4:${src}` },
76
+ }}
77
+ />
78
+ </Provider>,
79
+ );
80
+ expect(container.querySelector("iframe")).toBeNull();
81
+ const videos = container.querySelectorAll("video");
82
+ expect(videos).toHaveLength(2);
83
+ for (const video of videos) {
84
+ expect(video).toHaveAttribute("src", src);
85
+ }
86
+ });
87
+
88
+ it("updates an existing output after the user runs a cell", () => {
89
+ const { container } = renderVideo();
90
+ expect(container.querySelector("video")).not.toBeNull();
91
+ act(() => store.set(hasRunAnyCellAtom, true));
92
+ expect(container.querySelector("video")).toBeNull();
93
+ expect(container.querySelector("iframe")).toHaveAttribute(
94
+ "src",
95
+ "https://example.com/video.mp4",
96
+ );
97
+ });
98
+
99
+ it.each(["app", "auto-instantiate", "export"] as const)(
100
+ "preserves iframe output in a trusted %s context",
101
+ (context) => {
102
+ switch (context) {
103
+ case "app":
104
+ store.set(initialModeAtom, "read");
105
+ break;
106
+ case "auto-instantiate":
107
+ store.set(configOverridesAtom, {
108
+ runtime: { auto_instantiate: true },
109
+ });
110
+ break;
111
+ case "export":
112
+ window.__MARIMO_EXPORT_CONTEXT__ = { trusted: true };
113
+ break;
114
+ }
115
+ const { container } = renderVideo();
116
+ expect(container.querySelector("video")).toBeNull();
117
+ expect(container.querySelector("iframe")).toHaveAttribute(
118
+ "title",
119
+ "Video output",
120
+ );
121
+ expect(container.querySelector("iframe")).toHaveClass("output");
122
+ },
123
+ );
124
+
125
+ it("uses native video before the app mode is initialized", () => {
126
+ store.set(initialModeAtom, undefined);
127
+ const { container } = renderVideo();
128
+ expect(container.querySelector("video")).not.toBeNull();
129
+ expect(container.querySelector("iframe")).toBeNull();
130
+ });
131
+ });
@@ -0,0 +1,125 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ const logger = vi.hoisted(() => ({
5
+ log: vi.fn(),
6
+ warn: vi.fn(),
7
+ error: vi.fn(),
8
+ }));
9
+ vi.mock("@/utils/Logger", () => ({ Logger: logger }));
10
+ vi.mock("@/core/hotkeys/shortcuts", () => ({ isPlatformMac: () => true }));
11
+
12
+ describe("VS Code paste messages", () => {
13
+ const originalUrl = window.location.href;
14
+ const setData = vi.fn();
15
+ let parent: Window;
16
+ let child: Window;
17
+ let input: HTMLTextAreaElement;
18
+ let cleanupListeners: () => void;
19
+ const onPaste = vi.fn();
20
+
21
+ beforeEach(async () => {
22
+ vi.resetModules();
23
+ const parentFrame = document.createElement("iframe");
24
+ const childFrame = document.createElement("iframe");
25
+ input = document.createElement("textarea");
26
+ document.body.append(parentFrame, childFrame, input);
27
+ if (!parentFrame.contentWindow || !childFrame.contentWindow) {
28
+ throw new Error("Test frames need windows");
29
+ }
30
+ parent = parentFrame.contentWindow;
31
+ child = childFrame.contentWindow;
32
+ vi.stubGlobal("parent", parent);
33
+ vi.stubGlobal(
34
+ "DataTransfer",
35
+ class {
36
+ public setData = setData;
37
+ },
38
+ );
39
+ vi.stubGlobal(
40
+ "ClipboardEvent",
41
+ class extends Event {
42
+ public clipboardData: DataTransfer | undefined;
43
+ public constructor(type: string, init: ClipboardEventInit) {
44
+ super(type);
45
+ this.clipboardData = init.clipboardData ?? undefined;
46
+ }
47
+ },
48
+ );
49
+ window.history.replaceState(null, "", "?vscode=true");
50
+ input.addEventListener("paste", onPaste);
51
+ input.focus();
52
+ const windowListeners = vi.spyOn(window, "addEventListener");
53
+ const documentListeners = vi.spyOn(document, "addEventListener");
54
+ cleanupListeners = () => {
55
+ for (const [type, listener, options] of windowListeners.mock.calls) {
56
+ window.removeEventListener(type, listener, options);
57
+ }
58
+ for (const [type, listener, options] of documentListeners.mock.calls) {
59
+ document.removeEventListener(type, listener, options);
60
+ }
61
+ };
62
+ const { maybeRegisterVSCodeBindings } = await import("../vscode-bindings");
63
+ maybeRegisterVSCodeBindings();
64
+ });
65
+
66
+ afterEach(() => {
67
+ cleanupListeners();
68
+ vi.restoreAllMocks();
69
+ vi.unstubAllGlobals();
70
+ document.body.replaceChildren();
71
+ window.history.replaceState(null, "", originalUrl);
72
+ });
73
+
74
+ function send(data: unknown, source: Window | null = parent) {
75
+ window.dispatchEvent(
76
+ new MessageEvent("message", {
77
+ data,
78
+ source,
79
+ origin: "vscode-webview://dynamic-parent",
80
+ }),
81
+ );
82
+ }
83
+
84
+ it.each(["clipboard text", ""])(
85
+ "accepts parent paste with text %j",
86
+ (text) => {
87
+ send({ command: "paste", text });
88
+ expect(setData).toHaveBeenCalledWith("text/plain", text);
89
+ expect(onPaste).toHaveBeenCalledOnce();
90
+ expect(logger.error).not.toHaveBeenCalled();
91
+ },
92
+ );
93
+
94
+ it("rejects a child frame even when its reported origin matches", () => {
95
+ send({ command: "paste", text: "unsolicited" }, child);
96
+ expect(onPaste).not.toHaveBeenCalled();
97
+ expect(setData).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it("rejects a message without a source window", () => {
101
+ send({ command: "paste", text: "unsolicited" }, null);
102
+ expect(onPaste).not.toHaveBeenCalled();
103
+ });
104
+
105
+ it.each(
106
+ [
107
+ null,
108
+ [],
109
+ {},
110
+ "paste",
111
+ 1,
112
+ true,
113
+ { command: "paste" },
114
+ { command: "paste", text: {} },
115
+ { command: "paste", text: 12 },
116
+ { command: "paste", text: null },
117
+ { command: "execute", text: "unexpected" },
118
+ ].map((data) => ({ data })),
119
+ )("ignores malformed parent messages without errors: %j", ({ data }) => {
120
+ send(data);
121
+ expect(onPaste).not.toHaveBeenCalled();
122
+ expect(setData).not.toHaveBeenCalled();
123
+ expect(logger.error).not.toHaveBeenCalled();
124
+ });
125
+ });
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { Logger } from "@/utils/Logger";
4
+ import { isRecord } from "@/utils/records";
4
5
  import { KnownQueryParams } from "../constants";
5
6
  import { isPlatformMac } from "../hotkeys/shortcuts";
6
7
  import { isWasm } from "../wasm/utils";
@@ -53,9 +54,20 @@ function registerCopyPaste() {
53
54
  });
54
55
  });
55
56
 
56
- window.addEventListener("message", async (event) => {
57
+ window.addEventListener("message", async (event: MessageEvent<unknown>) => {
58
+ // VS Code assigns each parent webview a different origin.
59
+ if (event.source !== window.parent) {
60
+ return;
61
+ }
62
+ const message = event.data;
63
+ if (
64
+ !isRecord(message) ||
65
+ message.command !== "paste" ||
66
+ typeof message.text !== "string"
67
+ ) {
68
+ return;
69
+ }
57
70
  try {
58
- const message = event.data;
59
71
  const isMac = isPlatformMac();
60
72
  switch (message.command) {
61
73
  case "paste":
@@ -397,6 +397,7 @@ const AddTransformDropdown: React.FC<
397
397
  window.open(
398
398
  "https://github.com/marimo-team/marimo/issues/new?title=New%20dataframe%20transform:&labels=enhancement&template=feature_request.yaml",
399
399
  "_blank",
400
+ "noopener",
400
401
  );
401
402
  }}
402
403
  >