@marimo-team/islands 0.23.15-dev32 → 0.23.15-dev33

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.
@@ -36290,7 +36290,7 @@ ${d}`,
36290
36290
  return Logger.warn("Failed to get version from mount config"), null;
36291
36291
  }
36292
36292
  }
36293
- marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.15-dev32");
36293
+ marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.15-dev33");
36294
36294
  showCodeInRunModeAtom = atom(true);
36295
36295
  atom(null);
36296
36296
  var import_compiler_runtime = require_compiler_runtime();
package/dist/main.js CHANGED
@@ -26,7 +26,7 @@ import { $ as notebookOutline, $t as requestClientAtom, A as ChevronDownIcon, Bt
26
26
  import { __tla as __tla_1 } from "./chunk-5FQGJX7Z-BbqSm5gU.js";
27
27
  import { o as useSize, s as Root$1, u as createLucideIcon } from "./dist--2Bqjvs0.js";
28
28
  import { A as SquareFunction, C as DEFAULT_COLOR_SCHEME, D as SCALE_TYPE_DESCRIPTIONS, E as EMPTY_VALUE$1, O as TIME_UNIT_DESCRIPTIONS, S as DEFAULT_AGGREGATION, T as DEFAULT_TIME_UNIT, _ as AGGREGATION_TYPE_DESCRIPTIONS, a as AGGREGATION_FNS$1, b as COLOR_SCHEMES, c as COLOR_BY_FIELDS, d as NONE_VALUE, f as SELECTABLE_DATA_TYPES, g as TIME_UNITS, h as STRING_AGGREGATION_FNS, i as convertDataTypeToSelectable, j as ChartColumn, k as escapeFieldName, l as COMBINED_TIME_UNITS, m as SORT_TYPES, n as createSpecWithoutData, o as BIN_AGGREGATION, p as SINGLE_TIME_UNITS, r as isFieldSet, s as CHART_TYPES, t as augmentSpecWithData, u as ChartType, v as AGGREGATION_TYPE_ICON, w as DEFAULT_MAX_BINS_FACET, x as COUNT_FIELD, y as CHART_TYPE_ICON } from "./spec-DSs9v0xx.js";
29
- import { $ as ContextAwarePanelItem, $t as EmotionCacheProvider, A as prettifyRowColumnCount, At as loadTableData, B as useSelectList, Bt as dateToLocalISOTime, C as Filenames, Ct as ColumnChartContext, D as EmptyState, Dt as usePrevious$1, E as ColumnPreviewContainer, Et as useIntersectionObserver, F as ContextMenuContent, Ft as getMimeValues, G as ComboboxItem, Gt as ChartErrorState, H as DatePicker, Ht as TabsContent, I as ContextMenuItem, It as isNullishFilter, J as CommandInput, Jt as LazyVegaEmbed, K as Command, Kt as ChartInfoState, L as ContextMenuSeparator, Lt as Maps, M as getColumnCountForDisplay, Mt as SELECT_COLUMN_ID, N as getUserColumnVisibilityCounts, Nt as TOO_MANY_ROWS, O as ErrorState, Ot as getPageIndexForRow, P as ContextMenu, Pt as toFieldTypes, Q as smartMatch, Qt as HtmlOutput, R as ContextMenuTrigger, Rt as dateToLocalISODate, S as Progress, St as renderCellValue, T as ColumnName, Tt as DelayMount, U as DateRangePicker, Ut as TabsList, V as CompactChipRow, Vt as Tabs, W as Combobox, Wt as TabsTrigger, X as CommandList, Xt as RenderTextWithLinks, Y as CommandItem, Yt as useOverflowDetection, Z as CommandSeparator, Zt as Kbd, _ as InstallPackageButton, _n as ArrowDownWideNarrow, _t as TableHeader, an as GripHorizontal, at as SlotNames, b as downloadByURL, bt as generateColumns, d as RadioGroup, dn as Code, dt as Fill, en as hasFunctionProperty, et as PANEL_TYPES, f as RadioGroupItem, fn as ChevronsUpDown, ft as Provider$1, g as DataTable, gn as ChevronLeft, gt as TableHead, h as OutputRenderer, hn as ChevronsDownUp, ht as TableCell, in as TextWrap, it as isCellAwareAtom, j as prettifyRowCount, jt as INDEX_COLUMN_NAME, k as LoadingState, kt as loadTableAndRawData, l as Slide, ln as Ellipsis, m as OutputArea, mn as ChevronsLeft, mt as TableBody, n as marimoVersionAtom, nn as $fae977aafc393c5c$export$588937bcd60ade55, nt as contextAwarePanelOwner, on as Funnel, ot as slotsController, p as JsonOutput, pn as ChevronsRight, pt as Table, q as CommandEmpty, qt as ChartLoadingState, r as showCodeInRunModeAtom, rn as $fae977aafc393c5c$export$6b862160d295c8e, rt as contextAwarePanelType, s as SLIDE_TYPE_OPTIONS_BY_VALUE, sn as EyeOff, st as Toggle, t as useNotebookCodeAvailable, tn as isRecord, tt as contextAwarePanelOpen, u as Switch, un as Download, v as ADD_PRINTING_CLASS, vt as TableRow, w as downloadSizeLimitAtom, wt as ColumnChartSpecModel, x as downloadHTMLAsImage, xt as inferFieldTypes, y as downloadBlob, yt as NAMELESS_COLUMN_PREFIX, z as useInternalStateWithSync, zt as dateToLocalISODateTime, __tla as __tla_2 } from "./code-visibility-CEHWVGz1.js";
29
+ import { $ as ContextAwarePanelItem, $t as EmotionCacheProvider, A as prettifyRowColumnCount, At as loadTableData, B as useSelectList, Bt as dateToLocalISOTime, C as Filenames, Ct as ColumnChartContext, D as EmptyState, Dt as usePrevious$1, E as ColumnPreviewContainer, Et as useIntersectionObserver, F as ContextMenuContent, Ft as getMimeValues, G as ComboboxItem, Gt as ChartErrorState, H as DatePicker, Ht as TabsContent, I as ContextMenuItem, It as isNullishFilter, J as CommandInput, Jt as LazyVegaEmbed, K as Command, Kt as ChartInfoState, L as ContextMenuSeparator, Lt as Maps, M as getColumnCountForDisplay, Mt as SELECT_COLUMN_ID, N as getUserColumnVisibilityCounts, Nt as TOO_MANY_ROWS, O as ErrorState, Ot as getPageIndexForRow, P as ContextMenu, Pt as toFieldTypes, Q as smartMatch, Qt as HtmlOutput, R as ContextMenuTrigger, Rt as dateToLocalISODate, S as Progress, St as renderCellValue, T as ColumnName, Tt as DelayMount, U as DateRangePicker, Ut as TabsList, V as CompactChipRow, Vt as Tabs, W as Combobox, Wt as TabsTrigger, X as CommandList, Xt as RenderTextWithLinks, Y as CommandItem, Yt as useOverflowDetection, Z as CommandSeparator, Zt as Kbd, _ as InstallPackageButton, _n as ArrowDownWideNarrow, _t as TableHeader, an as GripHorizontal, at as SlotNames, b as downloadByURL, bt as generateColumns, d as RadioGroup, dn as Code, dt as Fill, en as hasFunctionProperty, et as PANEL_TYPES, f as RadioGroupItem, fn as ChevronsUpDown, ft as Provider$1, g as DataTable, gn as ChevronLeft, gt as TableHead, h as OutputRenderer, hn as ChevronsDownUp, ht as TableCell, in as TextWrap, it as isCellAwareAtom, j as prettifyRowCount, jt as INDEX_COLUMN_NAME, k as LoadingState, kt as loadTableAndRawData, l as Slide, ln as Ellipsis, m as OutputArea, mn as ChevronsLeft, mt as TableBody, n as marimoVersionAtom, nn as $fae977aafc393c5c$export$588937bcd60ade55, nt as contextAwarePanelOwner, on as Funnel, ot as slotsController, p as JsonOutput, pn as ChevronsRight, pt as Table, q as CommandEmpty, qt as ChartLoadingState, r as showCodeInRunModeAtom, rn as $fae977aafc393c5c$export$6b862160d295c8e, rt as contextAwarePanelType, s as SLIDE_TYPE_OPTIONS_BY_VALUE, sn as EyeOff, st as Toggle, t as useNotebookCodeAvailable, tn as isRecord, tt as contextAwarePanelOpen, u as Switch, un as Download, v as ADD_PRINTING_CLASS, vt as TableRow, w as downloadSizeLimitAtom, wt as ColumnChartSpecModel, x as downloadHTMLAsImage, xt as inferFieldTypes, y as downloadBlob, yt as NAMELESS_COLUMN_PREFIX, z as useInternalStateWithSync, zt as dateToLocalISODateTime, __tla as __tla_2 } from "./code-visibility-BI94mK4u.js";
30
30
  import { c as Calendar, i as createReducerAndAtoms, n as useOnUnmount, o as ToggleLeft, t as useOnMount } from "./useLifecycle-C6wHjkhW.js";
31
31
  import { t as Check } from "./check-C9OoNtR4.js";
32
32
  import { A as Icon, C as logNever, D as $18f2051aff69b9bf$export$a54013f0d02a8f82, E as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, F as createCollection, I as X, M as clamp$2, N as usePrevious$2, P as useDirection, R as ChevronDown, S as assertNever, a as SelectGroup, c as SelectSeparator, d as NativeSelect, i as SelectContent, j as Trigger$1, l as SelectTrigger, n as capitalize, o as SelectItem, r as Select, s as SelectLabel, t as Strings, u as SelectValue, w as $a916eb452884faea$export$b7a616150fdb9f44 } from "./strings-Dq_j3Rxw.js";
@@ -38024,7 +38024,7 @@ ${c}
38024
38024
  function _temp2$2(e) {
38025
38025
  e.target === e.currentTarget && e.key === "Enter" && (e.preventDefault(), e.stopPropagation(), e.currentTarget.click());
38026
38026
  }
38027
- var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-kLDhMoa2.js"));
38027
+ var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-ELidGmPF.js"));
38028
38028
  const SlidesLayoutRenderer = ({ layout: e, setLayout: r, cells: c, mode: l }) => {
38029
38029
  var _a5;
38030
38030
  let u = useAtomValue(kioskModeAtom), d = l === "read" || u, f = useAtomValue(numColumnsAtom) > 1, [p, m] = (0, import_react.useState)(null), { slideCells: h, skippedIds: g, noOutputIds: _, slideTypes: v, startCellIndex: y } = (0, import_react.useMemo)(() => computeSlideCellsInfo(c, e), [
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
9
9
  import { dt as kioskModeAtom, ht as outputIsStale } from "./html-to-image-D8Q5SLSJ.js";
10
10
  import "./chunk-5FQGJX7Z-BbqSm5gU.js";
11
11
  import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
12
- import { a as DEFAULT_DECK_VERTICAL_ALIGN, c as SlideSidebar, cn as Expand, ct as Panel, dn as Code, i as DEFAULT_DECK_TRANSITION, l as Slide, lt as PanelGroup, o as DEFAULT_SLIDE_TYPE, sn as EyeOff, t as useNotebookCodeAvailable, ut as PanelResizeHandle } from "./code-visibility-CEHWVGz1.js";
12
+ import { a as DEFAULT_DECK_VERTICAL_ALIGN, c as SlideSidebar, cn as Expand, ct as Panel, dn as Code, i as DEFAULT_DECK_TRANSITION, l as Slide, lt as PanelGroup, o as DEFAULT_SLIDE_TYPE, sn as EyeOff, t as useNotebookCodeAvailable, ut as PanelResizeHandle } from "./code-visibility-BI94mK4u.js";
13
13
  import { X as useDebouncedCallback } from "./input-BGPrFH3g.js";
14
14
  import "./toDate-CKpRx4TS.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.15-dev32",
3
+ "version": "0.23.15-dev33",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,176 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest";
4
+ import type * as MainModule from "../main";
5
+
6
+ // `main.ts` is the public islands entry point (see islands/vite.config.mts),
7
+ // so this suite snapshots its export surface + signatures and covers each
8
+ // entry point. The real bridge/bootstrap spin up a Web Worker and the plugin
9
+ // runtime, so we mock them.
10
+ const mockInitializeApps = vi.fn(() => Promise.resolve());
11
+ const mockStopSession = vi.fn((_appId?: string) => Promise.resolve());
12
+ const mockInitializeIslands = vi.fn(() => Promise.resolve());
13
+ const mockParseMarimoIslandApps = vi.fn(() => [] as Array<{ id: string }>);
14
+
15
+ vi.mock("@/core/islands/bridge", () => ({
16
+ getGlobalBridge: () => ({
17
+ initializeApps: mockInitializeApps,
18
+ stopSession: mockStopSession,
19
+ }),
20
+ }));
21
+
22
+ vi.mock("@/core/islands/bootstrap", () => ({
23
+ initializeIslands: mockInitializeIslands,
24
+ }));
25
+
26
+ vi.mock("@/core/islands/parse", () => ({
27
+ parseMarimoIslandApps: mockParseMarimoIslandApps,
28
+ }));
29
+
30
+ /**
31
+ * Re-import a fresh copy of `main.ts` so the module-level `bootstrapPromise`
32
+ * memoization does not leak between tests. The module auto-runs `initialize()`
33
+ * on load, so we clear the DOM first to keep that a no-op.
34
+ */
35
+ async function importMain(): Promise<typeof MainModule> {
36
+ vi.resetModules();
37
+ return import("../main");
38
+ }
39
+
40
+ beforeEach(() => {
41
+ document.body.replaceChildren();
42
+ mockInitializeApps.mockClear();
43
+ mockStopSession.mockClear();
44
+ mockInitializeIslands.mockClear();
45
+ mockParseMarimoIslandApps.mockReset();
46
+ mockParseMarimoIslandApps.mockReturnValue([]);
47
+ });
48
+
49
+ describe("islands public API surface", () => {
50
+ it("exposes a stable set of exports and signatures", async () => {
51
+ const mod = await importMain();
52
+ const surface = Object.fromEntries(
53
+ Object.keys(mod)
54
+ .toSorted()
55
+ .map((key) => {
56
+ const value = (mod as Record<string, unknown>)[key];
57
+ return [
58
+ key,
59
+ typeof value === "function"
60
+ ? `function(arity ${value.length})`
61
+ : typeof value,
62
+ ];
63
+ }),
64
+ );
65
+
66
+ expect(surface).toMatchInlineSnapshot(`
67
+ {
68
+ "canReplaceApp": "function(arity 0)",
69
+ "initialize": "function(arity 0)",
70
+ "stopApp": "function(arity 1)",
71
+ }
72
+ `);
73
+ });
74
+
75
+ it("matches the declared TypeScript signatures", () => {
76
+ expectTypeOf<typeof MainModule.canReplaceApp>().toEqualTypeOf<
77
+ () => boolean
78
+ >();
79
+ expectTypeOf<typeof MainModule.initialize>().toEqualTypeOf<
80
+ () => Promise<void>
81
+ >();
82
+ expectTypeOf<typeof MainModule.stopApp>().toEqualTypeOf<
83
+ (appId?: string) => Promise<void>
84
+ >();
85
+ });
86
+ });
87
+
88
+ describe("canReplaceApp", () => {
89
+ it("returns false when the document has no island element", async () => {
90
+ const mod = await importMain();
91
+
92
+ expect(mod.canReplaceApp()).toBe(false);
93
+ // Short-circuits on the DOM guard before parsing.
94
+ expect(mockParseMarimoIslandApps).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it("returns true for exactly one app without materializing the DOM", async () => {
98
+ const mod = await importMain();
99
+ document.body.innerHTML = `<marimo-island data-app-id="app-1"></marimo-island>`;
100
+ mockParseMarimoIslandApps.mockReturnValue([{ id: "app-1" }]);
101
+
102
+ expect(mod.canReplaceApp()).toBe(true);
103
+ // A probe must not mutate island attributes.
104
+ expect(mockParseMarimoIslandApps).toHaveBeenCalledWith(document, {
105
+ materialize: false,
106
+ });
107
+ });
108
+
109
+ it("returns false when more than one app is present", async () => {
110
+ const mod = await importMain();
111
+ document.body.innerHTML = `<marimo-island data-app-id="app-1"></marimo-island>`;
112
+ mockParseMarimoIslandApps.mockReturnValue([
113
+ { id: "app-1" },
114
+ { id: "app-2" },
115
+ ]);
116
+
117
+ expect(mod.canReplaceApp()).toBe(false);
118
+ });
119
+
120
+ it("returns false when the island parses into zero apps", async () => {
121
+ const mod = await importMain();
122
+ document.body.innerHTML = `<marimo-island></marimo-island>`;
123
+ mockParseMarimoIslandApps.mockReturnValue([]);
124
+
125
+ expect(mod.canReplaceApp()).toBe(false);
126
+ });
127
+ });
128
+
129
+ describe("stopApp", () => {
130
+ it("forwards the requested app id to the bridge", async () => {
131
+ const mod = await importMain();
132
+
133
+ await mod.stopApp("app-7");
134
+ expect(mockStopSession).toHaveBeenCalledWith("app-7");
135
+
136
+ await mod.stopApp();
137
+ expect(mockStopSession).toHaveBeenLastCalledWith(undefined);
138
+ });
139
+ });
140
+
141
+ describe("initialize", () => {
142
+ it("skips bootstrap and app start when no islands are present", async () => {
143
+ const mod = await importMain();
144
+
145
+ await mod.initialize();
146
+
147
+ expect(mockInitializeIslands).not.toHaveBeenCalled();
148
+ expect(mockInitializeApps).not.toHaveBeenCalled();
149
+ });
150
+
151
+ it("bootstraps once, marks islands, and starts apps on each call", async () => {
152
+ const mod = await importMain();
153
+ document.body.innerHTML = `<marimo-island></marimo-island>`;
154
+
155
+ await mod.initialize();
156
+ await mod.initialize();
157
+
158
+ expect(mockInitializeIslands).toHaveBeenCalledOnce();
159
+ expect(mockInitializeApps).toHaveBeenCalledTimes(2);
160
+ expect(
161
+ document.querySelector("marimo-island")?.classList.contains("marimo"),
162
+ ).toBe(true);
163
+ });
164
+
165
+ it("clears the memoized bootstrap when it fails so it can be retried", async () => {
166
+ const mod = await importMain();
167
+ document.body.innerHTML = `<marimo-island></marimo-island>`;
168
+ mockInitializeIslands.mockRejectedValueOnce(new Error("bootstrap failed"));
169
+
170
+ await expect(mod.initialize()).rejects.toThrow("bootstrap failed");
171
+ await mod.initialize();
172
+
173
+ expect(mockInitializeIslands).toHaveBeenCalledTimes(2);
174
+ expect(mockInitializeApps).toHaveBeenCalledOnce();
175
+ });
176
+ });
@@ -20,7 +20,31 @@ import { parseMarimoIslandApps } from "./parse";
20
20
  const bridge = getGlobalBridge();
21
21
  let bootstrapPromise: Promise<void> | undefined;
22
22
 
23
- /** Returns whether the current document can replace its app in this worker. */
23
+ /**
24
+ * Public entry point for the islands bundle.
25
+ *
26
+ * Islands auto-initialize on load. Hosts that keep the browser realm alive
27
+ * across client-side navigation (retaining this worker and its Pyodide
28
+ * environment) can drive page transitions with:
29
+ *
30
+ * ```ts
31
+ * if (canReplaceApp()) {
32
+ * await stopApp();
33
+ * updatePage();
34
+ * await initialize();
35
+ * }
36
+ * ```
37
+ *
38
+ * The retained lifecycle (`canReplaceApp`/`stopApp`) only applies to pages
39
+ * whose islands resolve to a single app; multi-app pages are not replaceable.
40
+ */
41
+
42
+ /**
43
+ * Returns whether the current document can replace its app in this worker.
44
+ *
45
+ * True only when the document contains exactly one replaceable app. This is a
46
+ * read-only probe: it does not materialize island payloads or mutate the DOM.
47
+ */
24
48
  export function canReplaceApp(): boolean {
25
49
  if (!document.querySelector(ISLAND_TAG_NAMES.ISLAND)) {
26
50
  return false;
@@ -30,6 +54,10 @@ export function canReplaceApp(): boolean {
30
54
 
31
55
  /**
32
56
  * Mounts marimo custom elements and starts the apps in the current document.
57
+ *
58
+ * Safe to call repeatedly, including after client-side navigation: the worker
59
+ * bootstrap is memoized (and cleared if it fails, so a later call retries),
60
+ * while app discovery re-runs on every call to pick up the current DOM.
33
61
  */
34
62
  export async function initialize(): Promise<void> {
35
63
  const islands = document.querySelectorAll<HTMLElement>(
@@ -49,7 +77,13 @@ export async function initialize(): Promise<void> {
49
77
  await bridge.initializeApps();
50
78
  }
51
79
 
52
- /** Stops the matching active app session. */
80
+ /**
81
+ * Stops the matching active app session.
82
+ *
83
+ * With no `appId`, stops the current retained single-app session; with an
84
+ * `appId`, stops it only if it matches the active app. No-op when there is no
85
+ * retained session to stop.
86
+ */
53
87
  export async function stopApp(appId?: string): Promise<void> {
54
88
  await bridge.stopSession(appId);
55
89
  }