@marimo-team/frontend 0.23.1-dev20 → 0.23.1-dev22

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/index.html CHANGED
@@ -66,7 +66,7 @@
66
66
  <marimo-server-token data-token="{{ server_token }}" hidden></marimo-server-token>
67
67
  <!-- /TODO -->
68
68
  <title>{{ title }}</title>
69
- <script type="module" crossorigin src="./assets/index-C9DyCFTe.js"></script>
69
+ <script type="module" crossorigin src="./assets/index-C7hH7rgL.js"></script>
70
70
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-D2MJg03u.js">
71
71
  <link rel="modulepreload" crossorigin href="./assets/chunk-LvLJmgfZ.js">
72
72
  <link rel="modulepreload" crossorigin href="./assets/react-Bj1aDYRI.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/frontend",
3
- "version": "0.23.1-dev20",
3
+ "version": "0.23.1-dev22",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -123,6 +123,7 @@
123
123
  "path-to-regexp": "^8.4.0",
124
124
  "plotly.js": "^3.3.1",
125
125
  "pyodide": "0.27.7",
126
+ "radix-ui": "1.4.3",
126
127
  "react-arborist": "^3.4.3",
127
128
  "react-aria": "3.47.0",
128
129
  "react-aria-components": "1.16.0",
@@ -159,8 +160,7 @@
159
160
  "vscode-jsonrpc": "^8.2.1",
160
161
  "vscode-languageserver-protocol": "^3.17.5",
161
162
  "web-vitals": "^4.2.4",
162
- "zod": "^4.3.6",
163
- "radix-ui": "1.4.3"
163
+ "zod": "^4.3.6"
164
164
  },
165
165
  "scripts": {
166
166
  "preinstall": "npx only-allow pnpm",
@@ -217,7 +217,7 @@
217
217
  "oxfmt": "^0.42.0",
218
218
  "oxlint": "^1.58.0",
219
219
  "postcss": "^8.5.6",
220
- "postcss-plugin-namespace": "^0.0.3",
220
+ "postcss-prefix-selector": "^2.1.1",
221
221
  "react": "^19.2.4",
222
222
  "react-compiler-runtime": "19.1.0-rc.3",
223
223
  "react-dom": "^19.2.4",
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import type { components } from "@marimo-team/marimo-api";
4
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import {
6
6
  cellId,
7
7
  requestId,
@@ -83,17 +83,7 @@ describe("IslandsPyodideBridge", () => {
83
83
 
84
84
  beforeEach(() => {
85
85
  vi.clearAllMocks();
86
- // Reset the singleton by clearing the window property
87
- // oxlint-disable-next-line typescript/no-explicit-any
88
- delete (window as any)._marimo_private_IslandsPyodideBridge;
89
- // Access the singleton - creates a fresh instance
90
- bridge = IslandsPyodideBridge.INSTANCE;
91
- });
92
-
93
- afterEach(() => {
94
- // Clean up singleton
95
- // oxlint-disable-next-line typescript/no-explicit-any
96
- delete (window as any)._marimo_private_IslandsPyodideBridge;
86
+ bridge = new IslandsPyodideBridge({ autoStartSessions: false });
97
87
  });
98
88
 
99
89
  describe("sendComponentValues", () => {
@@ -0,0 +1,348 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { ISLAND_DATA_ATTRIBUTES } from "@/core/islands/constants";
4
+ import {
5
+ extractIslandCodeFromEmbed,
6
+ parseIslandElement,
7
+ parseIslandElementsIntoApps,
8
+ parseMarimoIslandApps,
9
+ } from "../parse";
10
+ import {
11
+ buildIslandHTML,
12
+ createIslandHarness,
13
+ type IslandHarness,
14
+ } from "./test-utils.tsx";
15
+
16
+ let harness: IslandHarness;
17
+
18
+ afterEach(() => {
19
+ harness?.cleanup();
20
+ });
21
+
22
+ // ============================================================================
23
+ // Reactive vs Non-Reactive Parsing
24
+ // ============================================================================
25
+
26
+ describe("reactive vs non-reactive islands", () => {
27
+ it("should parse reactive islands into apps with code", () => {
28
+ harness = createIslandHarness(
29
+ buildIslandHTML([
30
+ { reactive: true, code: "x = 1", output: "<div>1</div>" },
31
+ { reactive: true, code: "y = 2", output: "<div>2</div>" },
32
+ ]),
33
+ );
34
+
35
+ const apps = parseMarimoIslandApps(harness.container);
36
+ expect(apps).toHaveLength(1);
37
+ expect(apps[0].cells).toHaveLength(2);
38
+ expect(apps[0].cells[0].code).toBe("x = 1");
39
+ expect(apps[0].cells[1].code).toBe("y = 2");
40
+ });
41
+
42
+ it("should skip non-reactive islands during parsing (no code sent to kernel)", () => {
43
+ harness = createIslandHarness(
44
+ buildIslandHTML([
45
+ { reactive: true, code: "x = 1", output: "<div>1</div>" },
46
+ { reactive: false, output: "<div>static content</div>" },
47
+ { reactive: true, code: "y = 2", output: "<div>2</div>" },
48
+ ]),
49
+ );
50
+
51
+ const apps = parseMarimoIslandApps(harness.container);
52
+ expect(apps).toHaveLength(1);
53
+ // Only the 2 reactive islands become cells
54
+ expect(apps[0].cells).toHaveLength(2);
55
+ expect(apps[0].cells[0].code).toBe("x = 1");
56
+ expect(apps[0].cells[1].code).toBe("y = 2");
57
+ });
58
+
59
+ it("should not set data-cell-idx on non-reactive islands", () => {
60
+ harness = createIslandHarness(
61
+ buildIslandHTML([
62
+ { reactive: true, code: "x = 1", output: "<div>1</div>" },
63
+ { reactive: false, output: "<div>static</div>" },
64
+ ]),
65
+ );
66
+
67
+ parseMarimoIslandApps(harness.container);
68
+
69
+ const [reactiveIsland, nonReactiveIsland] = harness.islands;
70
+
71
+ // Reactive island gets a cell index
72
+ expect(reactiveIsland.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe(
73
+ "0",
74
+ );
75
+ // Non-reactive island does NOT get a cell index
76
+ expect(
77
+ nonReactiveIsland.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
78
+ ).toBeNull();
79
+ });
80
+
81
+ it("should handle all-non-reactive islands (empty app list)", () => {
82
+ harness = createIslandHarness(
83
+ buildIslandHTML([
84
+ { reactive: false, output: "<div>static 1</div>" },
85
+ { reactive: false, output: "<div>static 2</div>" },
86
+ ]),
87
+ );
88
+
89
+ const apps = parseMarimoIslandApps(harness.container);
90
+ expect(apps).toHaveLength(0);
91
+ });
92
+ });
93
+
94
+ // ============================================================================
95
+ // extractIslandCodeFromEmbed
96
+ // ============================================================================
97
+
98
+ describe("extractIslandCodeFromEmbed with harness", () => {
99
+ it("should return code for reactive islands", () => {
100
+ harness = createIslandHarness(
101
+ buildIslandHTML([
102
+ { reactive: true, code: 'mo.md("hello")', output: "<div>hello</div>" },
103
+ ]),
104
+ );
105
+
106
+ const code = extractIslandCodeFromEmbed(harness.islands[0]);
107
+ expect(code).toBe('mo.md("hello")');
108
+ });
109
+
110
+ it("should return empty string for non-reactive islands", () => {
111
+ harness = createIslandHarness(
112
+ buildIslandHTML([
113
+ {
114
+ reactive: false,
115
+ code: 'mo.md("hello")',
116
+ output: "<div>hello</div>",
117
+ },
118
+ ]),
119
+ );
120
+
121
+ const code = extractIslandCodeFromEmbed(harness.islands[0]);
122
+ expect(code).toBe("");
123
+ });
124
+ });
125
+
126
+ // ============================================================================
127
+ // parseIslandElement
128
+ // ============================================================================
129
+
130
+ describe("parseIslandElement with harness", () => {
131
+ it("should return cell data for reactive island with output and code", () => {
132
+ harness = createIslandHarness(
133
+ buildIslandHTML([
134
+ { reactive: true, code: "x = 1", output: "<div>1</div>" },
135
+ ]),
136
+ );
137
+
138
+ const result = parseIslandElement(harness.islands[0]);
139
+ expect(result).not.toBeNull();
140
+ expect(result!.code).toBe("x = 1");
141
+ expect(result!.output).toBe("<div>1</div>");
142
+ });
143
+
144
+ it("should return null for non-reactive island (code is empty)", () => {
145
+ harness = createIslandHarness(
146
+ buildIslandHTML([{ reactive: false, output: "<div>static</div>" }]),
147
+ );
148
+
149
+ const result = parseIslandElement(harness.islands[0]);
150
+ expect(result).toBeNull();
151
+ });
152
+ });
153
+
154
+ // ============================================================================
155
+ // Multi-app parsing
156
+ // ============================================================================
157
+
158
+ describe("multi-app parsing with harness", () => {
159
+ it("should group islands by app-id", () => {
160
+ harness = createIslandHarness(
161
+ buildIslandHTML([
162
+ { appId: "app-1", reactive: true, code: "a = 1", output: "<div/>" },
163
+ { appId: "app-2", reactive: true, code: "b = 2", output: "<div/>" },
164
+ { appId: "app-1", reactive: true, code: "c = 3", output: "<div/>" },
165
+ ]),
166
+ );
167
+
168
+ const apps = parseMarimoIslandApps(harness.container);
169
+ expect(apps).toHaveLength(2);
170
+
171
+ const app1 = apps.find((a) => a.id === "app-1")!;
172
+ const app2 = apps.find((a) => a.id === "app-2")!;
173
+
174
+ expect(app1.cells).toHaveLength(2);
175
+ expect(app1.cells[0].code).toBe("a = 1");
176
+ expect(app1.cells[1].code).toBe("c = 3");
177
+
178
+ expect(app2.cells).toHaveLength(1);
179
+ expect(app2.cells[0].code).toBe("b = 2");
180
+ });
181
+
182
+ it("should assign sequential cell indices within each app", () => {
183
+ harness = createIslandHarness(
184
+ buildIslandHTML([
185
+ { appId: "app-1", reactive: true, code: "a", output: "<div/>" },
186
+ { appId: "app-1", reactive: true, code: "b", output: "<div/>" },
187
+ { appId: "app-1", reactive: true, code: "c", output: "<div/>" },
188
+ ]),
189
+ );
190
+
191
+ const apps = parseMarimoIslandApps(harness.container);
192
+ expect(apps[0].cells.map((c) => c.idx)).toEqual([0, 1, 2]);
193
+ });
194
+
195
+ it("should skip non-reactive islands in cell index assignment", () => {
196
+ harness = createIslandHarness(
197
+ buildIslandHTML([
198
+ { reactive: true, code: "a = 1", output: "<div/>" },
199
+ { reactive: false, output: "<div>static</div>" },
200
+ { reactive: true, code: "b = 2", output: "<div/>" },
201
+ ]),
202
+ );
203
+
204
+ const apps = parseMarimoIslandApps(harness.container);
205
+ expect(apps[0].cells).toHaveLength(2);
206
+ expect(apps[0].cells[0].idx).toBe(0);
207
+ expect(apps[0].cells[1].idx).toBe(1);
208
+
209
+ // Verify DOM: reactive islands get indices, non-reactive does not
210
+ expect(
211
+ harness.islands[0].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
212
+ ).toBe("0");
213
+ expect(
214
+ harness.islands[1].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
215
+ ).toBeNull();
216
+ expect(
217
+ harness.islands[2].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
218
+ ).toBe("1");
219
+ });
220
+ });
221
+
222
+ // ============================================================================
223
+ // Mixed reactive/non-reactive scenarios (regression tests)
224
+ // ============================================================================
225
+
226
+ describe("mixed reactive/non-reactive island scenarios", () => {
227
+ it("should handle the generate.py demo pattern: reactive + non-reactive + display_code", () => {
228
+ // Mirrors the "Island Features" section of generate.py
229
+ harness = createIslandHarness(
230
+ buildIslandHTML([
231
+ // Section header (reactive)
232
+ {
233
+ reactive: true,
234
+ code: 'mo.md("## Display Code")',
235
+ output: "<div><h2>Display Code</h2></div>",
236
+ },
237
+ // display_code island (reactive)
238
+ {
239
+ reactive: true,
240
+ code: 'mo.md("You can show the code")',
241
+ output: "<div>You can show the code</div>",
242
+ displayCode: true,
243
+ },
244
+ // Non-reactive section header
245
+ {
246
+ reactive: true,
247
+ code: 'mo.md("## Non-Reactive Islands")',
248
+ output: "<div><h2>Non-Reactive Islands</h2></div>",
249
+ },
250
+ // Non-reactive island — the one that was crashing
251
+ {
252
+ reactive: false,
253
+ code: 'mo.md("This island is non-reactive")',
254
+ output:
255
+ "<div>This island is non-reactive - it runs once and doesn't update</div>",
256
+ },
257
+ ]),
258
+ );
259
+
260
+ const apps = parseMarimoIslandApps(harness.container);
261
+
262
+ // Only 3 reactive islands become cells
263
+ expect(apps).toHaveLength(1);
264
+ expect(apps[0].cells).toHaveLength(3);
265
+
266
+ // Non-reactive island (index 3) has no cell-idx
267
+ expect(
268
+ harness.islands[3].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
269
+ ).toBeNull();
270
+ expect(
271
+ harness.islands[3].getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE),
272
+ ).toBe("false");
273
+ });
274
+
275
+ it("should handle non-reactive island at the start", () => {
276
+ harness = createIslandHarness(
277
+ buildIslandHTML([
278
+ { reactive: false, output: "<div>static header</div>" },
279
+ { reactive: true, code: "x = 1", output: "<div>1</div>" },
280
+ ]),
281
+ );
282
+
283
+ const apps = parseMarimoIslandApps(harness.container);
284
+ expect(apps).toHaveLength(1);
285
+ expect(apps[0].cells).toHaveLength(1);
286
+ expect(apps[0].cells[0].code).toBe("x = 1");
287
+ expect(apps[0].cells[0].idx).toBe(0);
288
+
289
+ // First island (non-reactive) has no index
290
+ expect(
291
+ harness.islands[0].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
292
+ ).toBeNull();
293
+ // Second island (reactive) gets index 0
294
+ expect(
295
+ harness.islands[1].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
296
+ ).toBe("0");
297
+ });
298
+
299
+ it("should handle alternating reactive and non-reactive islands", () => {
300
+ harness = createIslandHarness(
301
+ buildIslandHTML([
302
+ { reactive: true, code: "a = 1", output: "<div/>" },
303
+ { reactive: false, output: "<div>static</div>" },
304
+ { reactive: true, code: "b = 2", output: "<div/>" },
305
+ { reactive: false, output: "<div>static</div>" },
306
+ { reactive: true, code: "c = 3", output: "<div/>" },
307
+ ]),
308
+ );
309
+
310
+ const apps = parseMarimoIslandApps(harness.container);
311
+ expect(apps[0].cells).toHaveLength(3);
312
+ expect(apps[0].cells.map((c) => c.code)).toEqual([
313
+ "a = 1",
314
+ "b = 2",
315
+ "c = 3",
316
+ ]);
317
+ expect(apps[0].cells.map((c) => c.idx)).toEqual([0, 1, 2]);
318
+ });
319
+ });
320
+
321
+ // ============================================================================
322
+ // parseIslandElementsIntoApps (direct element-level tests)
323
+ // ============================================================================
324
+
325
+ describe("parseIslandElementsIntoApps with mixed elements", () => {
326
+ it("should preserve DOM order for cell indices", () => {
327
+ harness = createIslandHarness(
328
+ buildIslandHTML([
329
+ { reactive: true, code: "first", output: "<div/>" },
330
+ { reactive: true, code: "second", output: "<div/>" },
331
+ { reactive: true, code: "third", output: "<div/>" },
332
+ ]),
333
+ );
334
+
335
+ const apps = parseIslandElementsIntoApps(harness.islands);
336
+ expect(apps[0].cells.map((c) => c.code)).toEqual([
337
+ "first",
338
+ "second",
339
+ "third",
340
+ ]);
341
+ });
342
+
343
+ it("should handle empty container", () => {
344
+ harness = createIslandHarness("");
345
+ const apps = parseMarimoIslandApps(harness.container);
346
+ expect(apps).toHaveLength(0);
347
+ });
348
+ });