@marimo-team/islands 0.24.3-dev55 → 0.24.3-dev58

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.
Files changed (29) hide show
  1. package/dist/{ErrorBoundary-uQMMsMtf.js → ErrorBoundary-D1_JTlgF.js} +1 -1
  2. package/dist/assets/__vite-browser-external-C9iR87Zl.js +1 -0
  3. package/dist/assets/{worker-BemUy60C.js → worker-D7hWi02M.js} +2 -2
  4. package/dist/{code-visibility-DXFlGQTg.js → code-visibility-Ct164a9c.js} +1 -1
  5. package/dist/{common-DHIrhrev.js → common-B8x6sgCj.js} +1 -1
  6. package/dist/{data-grid-overlay-editor-DhUvLXgC.js → data-grid-overlay-editor-DOiNVNSM.js} +1 -1
  7. package/dist/{feedback-button-DejCYgl6.js → feedback-button-ajITqvHB.js} +2 -2
  8. package/dist/{glide-data-editor-BeDrnFzb.js → glide-data-editor-BZeeQUSi.js} +3 -3
  9. package/dist/{globals-DIuJGoqK.js → globals-S6heT9pz.js} +1 -1
  10. package/dist/main.js +42 -30
  11. package/dist/{number-overlay-editor-CKgrZ6UJ.js → number-overlay-editor-DSBousWi.js} +1 -1
  12. package/dist/{reveal-component-sLNrYulm.js → reveal-component-CegCLPOh.js} +2 -2
  13. package/package.json +1 -1
  14. package/src/core/islands/worker-factory.ts +2 -12
  15. package/src/core/wasm/__tests__/bootstrap.test.ts +63 -0
  16. package/src/core/wasm/__tests__/bridge.test.ts +8 -0
  17. package/src/core/wasm/__tests__/fs.test.ts +22 -0
  18. package/src/core/wasm/__tests__/runtime-config.test.ts +58 -0
  19. package/src/core/wasm/bridge.ts +18 -16
  20. package/src/core/wasm/rpc.ts +2 -0
  21. package/src/core/wasm/runtime-config.ts +27 -0
  22. package/src/core/wasm/worker/bootstrap.ts +21 -11
  23. package/src/core/wasm/worker/fs.ts +4 -0
  24. package/src/core/wasm/worker/save-worker.ts +7 -0
  25. package/src/core/wasm/worker/types.ts +7 -4
  26. package/src/core/wasm/worker/worker.ts +6 -0
  27. package/src/utils/__tests__/worker.test.ts +81 -0
  28. package/src/utils/worker.ts +22 -0
  29. package/dist/assets/__vite-browser-external-BajL8aoc.js +0 -1
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ import { C as T, F as E, G as D, I as O, J as k, K as A, L as j, P as M, R as N,
7
7
  import { a as se, f as ce, g as V, i as le, n as ue, o as de, p as fe, r as pe, t as me } from "./tooltip-qB8NlSqV.js";
8
8
  import { $ as he, $t as ge, At as _e, B as ve, C as ye, D as be, Dt as xe, F as Se, G as Ce, Gt as we, I as Te, J as Ee, L as De, Mt as Oe, Nt as ke, O as Ae, Ot as je, P as Me, Q as Ne, Qt as Pe, S as Fe, T as Ie, Tt as Le, U as Re, Ut as ze, W as Be, Wt as Ve, X as He, Xt as Ue, Y as We, Yt as Ge, Zt as Ke, _n as qe, _t as Je, an as Ye, bn as Xe, c as Ze, cn as Qe, ct as $e, d as et, dn as tt, dt as nt, f as rt, fn as it, ft as at, g as ot, gn as st, gt as ct, h as lt, in as ut, it as dt, jt as ft, k as pt, kt as mt, l as ht, m as gt, mn as _t, n as vt, o as yt, on as bt, p as xt, pn as St, pt as Ct, q as wt, qt as Tt, r as Et, rn as Dt, rt as Ot, u as kt, v as At, w as jt, xt as Mt, yn as Nt, zt as Pt } from "./cells-BSgNCBZ8.js";
9
9
  import { D as Ft, E as It, O as Lt, S as Rt, T as zt, _ as Bt, a as Vt, c as Ht, d as Ut, f as Wt, g as Gt, h as Kt, i as qt, l as Jt, m as Yt, n as Xt, o as Zt, p as Qt, r as $t, s as en, t as tn, u as nn, v as rn, w as an, x as on, y as sn } from "./spec-CAYwFeV6.js";
10
- import { $t as cn, A as ln, At as un, B as dn, Bt as fn, C as pn, Ct as mn, D as hn, Dt as gn, E as _n, Et as vn, F as yn, Ft as bn, G as xn, Gt as Sn, H as Cn, Ht as wn, I as Tn, It as En, J as Dn, Jt as On, K as kn, Kt as An, L as jn, Lt as Mn, M as Nn, Mt as Pn, N as Fn, Nt as In, O as Ln, Ot as Rn, P as zn, Pt as Bn, Qt as Vn, R as Hn, Rt as Un, S as Wn, St as Gn, T as Kn, Tt as qn, U as Jn, Ut as Yn, V as Xn, Vt as Zn, W as Qn, Wt as $n, X as er, Xt as tr, Y as nr, Yt as rr, Zt as ir, _ as ar, _t as or, an as sr, at as cr, b as lr, c as ur, cn as dr, ct as fr, d as pr, dn as mr, dt as hr, en as gr, et as _r, f as vr, fn as yr, ft as br, g as xr, gt as Sr, h as Cr, hn as wr, ht as Tr, in as Er, it as Dr, j as Or, jt as kr, k as Ar, kt as jr, ln as Mr, lt as Nr, m as Pr, mn as Fr, mt as Ir, nn as Lr, nt as Rr, ot as zr, p as Br, pn as Vr, pt as Hr, q as Ur, qt as Wr, r as Gr, rn as Kr, rt as qr, sn as Jr, st as Yr, t as Xr, tn as Zr, tt as Qr, u as $r, un as ei, ut as ti, v as ni, vt as ri, w as ii, wt as ai, x as oi, y as si, yt as ci, z as li, zt as ui } from "./common-DHIrhrev.js";
10
+ import { $t as cn, A as ln, At as un, B as dn, Bt as fn, C as pn, Ct as mn, D as hn, Dt as gn, E as _n, Et as vn, F as yn, Ft as bn, G as xn, Gt as Sn, H as Cn, Ht as wn, I as Tn, It as En, J as Dn, Jt as On, K as kn, Kt as An, L as jn, Lt as Mn, M as Nn, Mt as Pn, N as Fn, Nt as In, O as Ln, Ot as Rn, P as zn, Pt as Bn, Qt as Vn, R as Hn, Rt as Un, S as Wn, St as Gn, T as Kn, Tt as qn, U as Jn, Ut as Yn, V as Xn, Vt as Zn, W as Qn, Wt as $n, X as er, Xt as tr, Y as nr, Yt as rr, Zt as ir, _ as ar, _t as or, an as sr, at as cr, b as lr, c as ur, cn as dr, ct as fr, d as pr, dn as mr, dt as hr, en as gr, et as _r, f as vr, fn as yr, ft as br, g as xr, gt as Sr, h as Cr, hn as wr, ht as Tr, in as Er, it as Dr, j as Or, jt as kr, k as Ar, kt as jr, ln as Mr, lt as Nr, m as Pr, mn as Fr, mt as Ir, nn as Lr, nt as Rr, ot as zr, p as Br, pn as Vr, pt as Hr, q as Ur, qt as Wr, r as Gr, rn as Kr, rt as qr, sn as Jr, st as Yr, t as Xr, tn as Zr, tt as Qr, u as $r, un as ei, ut as ti, v as ni, vt as ri, w as ii, wt as ai, x as oi, y as si, yt as ci, z as li, zt as ui } from "./common-B8x6sgCj.js";
11
11
  import { c as di, i as fi, n as pi, o as mi, t as hi } from "./useLifecycle-DPgiiwwU.js";
12
12
  import { t as gi } from "./check-B7cIK6gb.js";
13
13
  import { A as _i, C as vi, D as yi, E as bi, M as xi, O as Si, S as Ci, T as wi, a as Ti, b as Ei, c as Di, i as Oi, k as ki, l as Ai, n as ji, o as Mi, r as Ni, s as Pi, t as Fi, w as Ii, x as Li } from "./select-C6plhmZr.js";
@@ -15,8 +15,8 @@ import { B as Ri, C as zi, J as Bi, Mt as Vi, Nt as Hi, S as Ui, Vt as Wi, W as
15
15
  import { _ as ha, c as ga, d as _a, f as va, g as ya, h as ba, m as xa, u as Sa, v as Ca } from "./toDate-WXg1yLcI.js";
16
16
  import { a as wa, c as Ta, d as Ea, f as Da, i as Oa, l as ka, n as Aa, o as ja, r as Ma, s as Na, t as Pa } from "./process-output-DFe2tVIY.js";
17
17
  import { a as Fa, c as Ia, d as La, i as Ra, l as za, r as Ba, u as Va } from "./glide-utils-B3rUT5PI.js";
18
- import { n as Ha, t as Ua } from "./ErrorBoundary-uQMMsMtf.js";
19
- import { n as Wa, t as Ga } from "./globals-DIuJGoqK.js";
18
+ import { n as Ha, t as Ua } from "./ErrorBoundary-D1_JTlgF.js";
19
+ import { n as Wa, t as Ga } from "./globals-S6heT9pz.js";
20
20
  import { t as Ka } from "./react-dom-BUgJ-ab_.js";
21
21
  import { t as qa } from "./jsx-runtime-DgDPJV9S.js";
22
22
  import { B as Ja, C as H, Ct as Ya, D as Xa, E as Za, Et as Qa, F as U, G as $a, H as eo, I as to, L as no, M as W, N as ro, O as io, P as ao, Q as oo, R as so, S as G, St as co, Tt as lo, U as uo, W as fo, Y as po, Z as mo, _ as ho, a as go, b as _o, bt as vo, c as yo, d as bo, dt as xo, f as So, g as Co, gt as wo, h as To, i as Eo, j as K, k as Do, l as Oo, m as ko, n as Ao, o as jo, p as Mo, r as No, s as Po, u as Fo, v as Io, vt as Lo, w as Ro, wt as zo, x as Bo, xt as Vo, y as Ho, yt as Uo } from "./use-restore-focus-D5D9xEjZ.js";
@@ -40,7 +40,7 @@ import { t as Ns } from "./useAsyncData-BD6AStMt.js";
40
40
  import { i as Ps, n as Fs, r as Is } from "./formats-BR5iGb4s.js";
41
41
  import { M as Ls, N as Rs } from "./vega-loader.browser-D95WfUcj.js";
42
42
  import { a as zs, n as Bs } from "./loader-D2W4oM-3.js";
43
- import { c as Vs, d as Hs, i as Us, l as Ws, o as Gs, s as Ks, t as qs, u as Js } from "./code-visibility-DXFlGQTg.js";
43
+ import { c as Vs, d as Hs, i as Us, l as Ws, o as Gs, s as Ks, t as qs, u as Js } from "./code-visibility-Ct164a9c.js";
44
44
  import { t as Ys } from "./useDeepCompareMemoize-D_HNdyvI.js";
45
45
  import { t as Xs } from "./esm-BElI3qKH.js";
46
46
  //#region ../node_modules/.pnpm/iconify-icon@2.3.0/node_modules/iconify-icon/dist/iconify-icon.mjs
@@ -10458,7 +10458,7 @@ function RC(e, t) {
10458
10458
  }
10459
10459
  //#endregion
10460
10460
  //#region src/plugins/impl/DataEditorPlugin.tsx
10461
- var zC = J.lazy(() => import("./glide-data-editor-BeDrnFzb.js").then((e) => e.t)), BC = xS("marimo-data-editor", { cssStyles: [WS] }).withData(W({
10461
+ var zC = J.lazy(() => import("./glide-data-editor-BZeeQUSi.js").then((e) => e.t)), BC = xS("marimo-data-editor", { cssStyles: [WS] }).withData(W({
10462
10462
  initialValue: W({ edits: G(W({
10463
10463
  rowIdx: K(),
10464
10464
  columnId: U(),
@@ -29922,7 +29922,7 @@ function bR(e) {
29922
29922
  }
29923
29923
  //#endregion
29924
29924
  //#region src/components/editor/renderers/slides-layout/slides-layout.tsx
29925
- var xR = J.lazy(() => import("./reveal-component-sLNrYulm.js")), SR = (e) => {
29925
+ var xR = J.lazy(() => import("./reveal-component-CegCLPOh.js")), SR = (e) => {
29926
29926
  let t = (0, X.c)(33), { layout: n, setLayout: r, cells: i, mode: a } = e, o = rs(Ce), s = a === "read" || o, c = rs(xt) > 1, [l, u] = (0, J.useState)(null), d;
29927
29927
  t[0] !== i || t[1] !== n ? (d = vP(i, n), t[0] = i, t[1] = n, t[2] = d) : d = t[2];
29928
29928
  let { slideCells: f, skippedIds: p, noOutputIds: m, slideTypes: h, startCellIndex: g } = d, _;
@@ -31916,31 +31916,43 @@ function DB(e) {
31916
31916
  });
31917
31917
  }
31918
31918
  //#endregion
31919
+ //#region src/utils/worker.ts
31920
+ function OB(e, t) {
31921
+ if (e.origin === globalThis.origin) return new Worker(e.href, {
31922
+ ...t,
31923
+ type: "module"
31924
+ });
31925
+ let n = `import ${JSON.stringify(e.href)}`, r = globalThis.origin === "null" ? `data:application/javascript,${encodeURIComponent(n)}` : URL.createObjectURL(new Blob([n], { type: "application/javascript" }));
31926
+ try {
31927
+ return new Worker(r, {
31928
+ ...t,
31929
+ type: "module"
31930
+ });
31931
+ } finally {
31932
+ r.startsWith("blob:") && URL.revokeObjectURL(r);
31933
+ }
31934
+ }
31935
+ //#endregion
31919
31936
  //#region src/core/islands/worker/worker.tsx?worker&url
31920
- var OB = "/assets/worker-BemUy60C.js", kB = class {
31937
+ var kB = "/assets/worker-D7hWi02M.js", AB = class {
31921
31938
  url;
31922
31939
  name;
31923
31940
  constructor(e = {}) {
31924
31941
  this.url = e.workerUrl || this.getDefaultWorkerUrl(), this.name = e.workerName || Ga();
31925
31942
  }
31926
31943
  create() {
31927
- let e = `import ${JSON.stringify(new URL(this.url, import.meta.url))}`, t = new Blob([e], { type: "application/javascript" }), n = URL.createObjectURL(t), r = new Worker(n, {
31928
- type: "module",
31929
- /* @vite-ignore */
31930
- name: this.name
31931
- });
31932
- return URL.revokeObjectURL(n), r;
31944
+ return OB(new URL(this.url, import.meta.url), { name: this.name });
31933
31945
  }
31934
31946
  getDefaultWorkerUrl() {
31935
- return AB(OB);
31947
+ return jB(kB);
31936
31948
  }
31937
31949
  };
31938
- function AB(e) {
31950
+ function jB(e) {
31939
31951
  return e.startsWith("./") ? e : e.startsWith("/") ? `.${e}` : `./${e}`;
31940
31952
  }
31941
31953
  //#endregion
31942
31954
  //#region src/core/islands/bridge.ts
31943
- var jB = class {
31955
+ var MB = class {
31944
31956
  rpc;
31945
31957
  messageConsumer;
31946
31958
  store;
@@ -31955,7 +31967,7 @@ var jB = class {
31955
31967
  constructor(e = {}) {
31956
31968
  this.store = e.store || os, this.root = e.root || document;
31957
31969
  try {
31958
- let t = (e.workerFactory || new kB()).create();
31970
+ let t = (e.workerFactory || new AB()).create();
31959
31971
  this.rpc = DB(t), this.setupMessageListeners();
31960
31972
  } catch (e) {
31961
31973
  throw c.error("Failed to initialize IslandsPyodideBridge:", e), this.initialized.reject(e), e;
@@ -32132,30 +32144,30 @@ var jB = class {
32132
32144
  return await e, t ?? n.promise;
32133
32145
  }
32134
32146
  destroy() {}
32135
- }, MB = null;
32136
- function NB() {
32137
- return MB ||= new jB(), MB;
32147
+ }, NB = null;
32148
+ function PB() {
32149
+ return NB ||= new MB(), NB;
32138
32150
  }
32139
32151
  //#endregion
32140
32152
  //#region src/core/islands/main.ts
32141
- var PB = NB(), FB;
32142
- function IB() {
32153
+ var FB = PB(), IB;
32154
+ function LB() {
32143
32155
  return document.querySelector(yu.ISLAND) ? Lz(document, { materialize: !1 }).length === 1 : !1;
32144
32156
  }
32145
- async function LB() {
32157
+ async function RB() {
32146
32158
  let e = document.querySelectorAll(yu.ISLAND);
32147
32159
  if (e.length !== 0) {
32148
32160
  for (let t of e) t.classList.add(Cu.NAMESPACE);
32149
- FB ??= mB({ bridge: PB }).catch((e) => {
32150
- throw FB = void 0, e;
32151
- }), await FB, await PB.initializeApps();
32161
+ IB ??= mB({ bridge: FB }).catch((e) => {
32162
+ throw IB = void 0, e;
32163
+ }), await IB, await FB.initializeApps();
32152
32164
  }
32153
32165
  }
32154
- async function RB(e) {
32155
- await PB.stopSession(e);
32166
+ async function zB(e) {
32167
+ await FB.stopSession(e);
32156
32168
  }
32157
- LB().catch((e) => {
32169
+ RB().catch((e) => {
32158
32170
  c.error("Failed to initialize islands:", e);
32159
32171
  });
32160
32172
  //#endregion
32161
- export { IB as canReplaceApp, LB as initialize, RB as stopApp };
32173
+ export { LB as canReplaceApp, RB as initialize, zB as stopApp };
@@ -1,6 +1,6 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { t } from "./react-ofJbxM4x.js";
3
- import { a as n } from "./glide-data-editor-BeDrnFzb.js";
3
+ import { a as n } from "./glide-data-editor-BZeeQUSi.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.2.8_b9e6aec46d952f631ce71b0585d2a8e5/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",
@@ -4,13 +4,13 @@ import { t as o } from "./react-ofJbxM4x.js";
4
4
  import { t as s } from "./compiler-runtime-Cj8ZWiNS.js";
5
5
  import { g as c, t as l } from "./tooltip-qB8NlSqV.js";
6
6
  import { G as u, w as d } from "./cells-BSgNCBZ8.js";
7
- import { $ as f, Q as p, Z as m, an as h, i as g, l as _, ln as v, n as y, on as b, r as x, t as S, u as C } from "./common-DHIrhrev.js";
7
+ import { $ as f, Q as p, Z as m, an as h, i as g, l as _, ln as v, n as y, on as b, r as x, t as S, u as C } from "./common-B8x6sgCj.js";
8
8
  import { M as w } from "./select-C6plhmZr.js";
9
9
  import { J as T } from "./dropdown-menu-CZgjYSAh.js";
10
10
  import { t as E } from "./jsx-runtime-DgDPJV9S.js";
11
11
  import { q as ee } from "./use-restore-focus-D5D9xEjZ.js";
12
12
  import { A as D, C as O, b as k } from "./useTheme-DzXmYZ5V.js";
13
- import { a as A, n as te, t as ne } from "./code-visibility-DXFlGQTg.js";
13
+ import { a as A, n as te, t as ne } from "./code-visibility-Ct164a9c.js";
14
14
  /* empty css */
15
15
  var j = c("sticky-note", [["path", {
16
16
  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.24.3-dev55",
3
+ "version": "0.24.3-dev58",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { createModuleWorker } from "@/utils/worker";
3
4
  import { getMarimoVersion } from "../meta/globals";
4
5
  import workerUrl from "./worker/worker.tsx?worker&url";
5
6
 
@@ -47,20 +48,9 @@ export class DefaultWorkerFactory implements WorkerFactory {
47
48
  * Creates a new Pyodide worker
48
49
  */
49
50
  create(): Worker {
50
- const js = `import ${JSON.stringify(new URL(this.url, import.meta.url))}`;
51
- const blob = new Blob([js], { type: "application/javascript" });
52
- const objURL = URL.createObjectURL(blob);
53
-
54
- const worker = new Worker(objURL, {
55
- type: "module",
56
- /* @vite-ignore */
51
+ return createModuleWorker(new URL(this.url, import.meta.url), {
57
52
  name: this.name,
58
53
  });
59
-
60
- // Blob URL can be revoked once the worker has loaded the script
61
- URL.revokeObjectURL(objURL);
62
-
63
- return worker;
64
54
  }
65
55
 
66
56
  /**
@@ -0,0 +1,63 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { loadPyodide } from "pyodide";
4
+ import type { PyodideInterface } from "pyodide";
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { DefaultWasmController } from "../worker/bootstrap";
7
+
8
+ vi.mock("pyodide", () => ({ loadPyodide: vi.fn() }));
9
+ vi.mock("../worker/getMarimoWheel", () => ({
10
+ getMarimoWheel: () => "marimo-base",
11
+ }));
12
+
13
+ const micropip = { set_index_urls: vi.fn(), destroy: vi.fn() };
14
+ const pyodide = { pyimport: vi.fn(() => micropip) };
15
+
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ vi.mocked(loadPyodide).mockResolvedValue(
19
+ pyodide as unknown as PyodideInterface,
20
+ );
21
+ });
22
+
23
+ describe("WASM runtime sources", () => {
24
+ it("preserves the default runtime sources", async () => {
25
+ await new DefaultWasmController().bootstrap({
26
+ version: "0.24.2",
27
+ pyodideVersion: "v314.0.0",
28
+ });
29
+ expect(loadPyodide).toHaveBeenCalledWith(
30
+ expect.objectContaining({
31
+ indexURL: "https://cdn.jsdelivr.net/pyodide/v314.0.0/full/",
32
+ packageBaseUrl: "https://cdn.jsdelivr.net/pyodide/v314.0.0/full/",
33
+ lockFileURL:
34
+ "https://wasm.marimo.app/pyodide-lock.json?v=0.24.2&pyodide=v314.0.0",
35
+ }),
36
+ );
37
+ expect(pyodide.pyimport).not.toHaveBeenCalled();
38
+ });
39
+
40
+ it("configures runtime and package sources before returning the interpreter", async () => {
41
+ const controller = new DefaultWasmController();
42
+ await controller.bootstrap({
43
+ version: "0.24.2",
44
+ pyodideVersion: "v314.0.0",
45
+ pyodideIndexUrl: "https://example.com/runtime/",
46
+ pyodideLockfileUrl: "https://example.com/lock.json",
47
+ pypiIndexUrl: "https://example.com/pypi/{package_name}/json",
48
+ });
49
+ expect(loadPyodide).toHaveBeenCalledWith(
50
+ expect.objectContaining({
51
+ indexURL: "https://example.com/runtime/",
52
+ packageBaseUrl: "https://example.com/runtime/",
53
+ lockFileURL: "https://example.com/lock.json",
54
+ }),
55
+ );
56
+ expect(pyodide.pyimport).toHaveBeenCalledWith("micropip");
57
+ expect(micropip.set_index_urls).toHaveBeenCalledWith(
58
+ "https://example.com/pypi/{package_name}/json",
59
+ );
60
+ expect(micropip.destroy).toHaveBeenCalledOnce();
61
+ expect(controller.requirePyodide).toBe(pyodide);
62
+ });
63
+ });
@@ -9,12 +9,14 @@ const {
9
9
  mockNotebookReadFile,
10
10
  mockReadNotebook,
11
11
  mockSaveNotebook,
12
+ mockBootstrap,
12
13
  rpcListeners,
13
14
  } = vi.hoisted(() => ({
14
15
  mockBridge: vi.fn(),
15
16
  mockNotebookReadFile: vi.fn(),
16
17
  mockReadNotebook: vi.fn(),
17
18
  mockSaveNotebook: vi.fn(),
19
+ mockBootstrap: vi.fn(),
18
20
  rpcListeners: {} as Record<string, () => void>,
19
21
  }));
20
22
 
@@ -40,6 +42,7 @@ vi.stubGlobal("URL", MockURL);
40
42
 
41
43
  vi.mock("@/core/wasm/rpc", () => ({
42
44
  getWorkerRPC: () => ({
45
+ send: { bootstrap: mockBootstrap },
43
46
  proxy: {
44
47
  request: {
45
48
  bridge: mockBridge,
@@ -122,6 +125,11 @@ describe("PyodideBridge.readCode", () => {
122
125
 
123
126
  await PyodideBridge.INSTANCE.readCode();
124
127
 
128
+ expect(mockBootstrap).toHaveBeenCalledWith({
129
+ pyodideIndexUrl: undefined,
130
+ pyodideLockfileUrl: undefined,
131
+ pypiIndexUrl: undefined,
132
+ });
125
133
  expect(mockNotebookReadFile).not.toHaveBeenCalled();
126
134
  });
127
135
  });
@@ -0,0 +1,22 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { PyodideInterface } from "pyodide";
3
+ import { afterEach, expect, it, vi } from "vitest";
4
+ import { WasmFileSystem } from "../worker/fs";
5
+
6
+ afterEach(() => vi.unstubAllGlobals());
7
+
8
+ it.each(["https://notebook.example", "null"])(
9
+ "mounts persistent storage only for a non-opaque origin: %s",
10
+ (origin) => {
11
+ vi.stubGlobal("origin", origin);
12
+ const mount = vi.fn();
13
+ const IDBFS = {};
14
+ const pyodide = {
15
+ FS: { mount, filesystems: { IDBFS } },
16
+ } as unknown as PyodideInterface;
17
+ WasmFileSystem.mountFS(pyodide);
18
+ expect(mount.mock.calls).toEqual(
19
+ origin === "null" ? [] : [[IDBFS, { root: "." }, "/marimo"]],
20
+ );
21
+ },
22
+ );
@@ -0,0 +1,58 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { afterEach, assert, describe, expect, it } from "vitest";
4
+ import { getWasmRuntimeConfig } from "../runtime-config";
5
+
6
+ afterEach(() => {
7
+ document.head.innerHTML = "";
8
+ });
9
+
10
+ describe("getWasmRuntimeConfig", () => {
11
+ it("uses runtime defaults when no URLs are configured", () => {
12
+ expect(getWasmRuntimeConfig()).toEqual({
13
+ pyodideIndexUrl: undefined,
14
+ pyodideLockfileUrl: undefined,
15
+ pypiIndexUrl: undefined,
16
+ });
17
+ });
18
+
19
+ it("resolves URLs against the page base and preserves micropip placeholders", () => {
20
+ document.head.innerHTML = `
21
+ <base href="https://example.com/blog/post/">
22
+ <marimo-wasm
23
+ data-pyodide-index-url="../runtime/"
24
+ data-pyodide-lockfile-url="/locks/pyodide.json?v=1&amp;x=2"
25
+ data-pypi-index-url="https://packages.example.com/{package_name}/json"
26
+ ></marimo-wasm>`;
27
+ expect(getWasmRuntimeConfig()).toEqual({
28
+ pyodideIndexUrl: "https://example.com/blog/runtime/",
29
+ pyodideLockfileUrl: "https://example.com/locks/pyodide.json?v=1&x=2",
30
+ pypiIndexUrl: "https://packages.example.com/{package_name}/json",
31
+ });
32
+ });
33
+
34
+ it.each(["pyodideIndexUrl", "pyodideLockfileUrl", "pypiIndexUrl"])(
35
+ "ignores malformed %s while preserving valid overrides",
36
+ (key) => {
37
+ document.head.innerHTML = `
38
+ <base href="https://example.com/notebook/">
39
+ <marimo-wasm
40
+ data-pyodide-index-url="./pyodide/"
41
+ data-pyodide-lockfile-url="./lockfile/pyodide.json"
42
+ data-pypi-index-url="./packages/{package_name}.json"
43
+ ></marimo-wasm>`;
44
+ const element = document.querySelector<HTMLElement>("marimo-wasm");
45
+ assert(element);
46
+ element.dataset[key] = "https://invalid host/";
47
+
48
+ expect(getWasmRuntimeConfig()).toEqual({
49
+ pyodideIndexUrl: "https://example.com/notebook/pyodide/",
50
+ pyodideLockfileUrl:
51
+ "https://example.com/notebook/lockfile/pyodide.json",
52
+ pypiIndexUrl:
53
+ "https://example.com/notebook/packages/{package_name}.json",
54
+ [key]: undefined,
55
+ });
56
+ },
57
+ );
58
+ });
@@ -9,6 +9,7 @@ import { throwNotImplemented } from "@/utils/functions";
9
9
  import { Logger } from "@/utils/Logger";
10
10
  import { reloadSafe } from "@/utils/reload-safe";
11
11
  import { generateUUID } from "@/utils/uuid";
12
+ import { createModuleWorker } from "@/utils/worker";
12
13
  import { notebookIsRunningAtom } from "../cells/cells";
13
14
  import type { CommandMessage } from "../kernel/messages";
14
15
  import { getMarimoVersion } from "../meta/globals";
@@ -41,10 +42,13 @@ import { BasicTransport } from "../websocket/transports/basic";
41
42
  import type { IConnectionTransport } from "../websocket/transports/transport";
42
43
  import { PyodideRouter } from "./router";
43
44
  import { getWorkerRPC } from "./rpc";
45
+ import { getWasmRuntimeConfig } from "./runtime-config";
44
46
  import { createShareableLink } from "./share";
45
47
  import { wasmInitStateAtom } from "./state";
46
48
  import { fallbackFileStore, notebookFileStore } from "./store";
47
49
  import { isWasm } from "./utils";
50
+ import saveWorkerUrl from "./worker/save-worker.ts?worker&url";
51
+ import workerUrl from "./worker/worker.ts?worker&url";
48
52
  import type { SaveWorkerSchema } from "./worker/save-worker";
49
53
  import type { WorkerSchema } from "./worker/worker";
50
54
 
@@ -81,19 +85,20 @@ export class PyodideBridge implements RunRequests, EditRequests {
81
85
  };
82
86
  }
83
87
 
88
+ const runtimeConfig = getWasmRuntimeConfig();
89
+
84
90
  // Create save worker
85
- const saveWorker = new Worker(
86
- // oxlint-disable-next-line unicorn/relative-url-style
87
- new URL("./worker/save-worker.ts", import.meta.url),
91
+ const saveWorker = createModuleWorker(
92
+ new URL(saveWorkerUrl, import.meta.url),
88
93
  {
89
- type: "module",
90
94
  // Pass the version (and optional capability suffix) to the worker
91
- /* @vite-ignore */
92
95
  name: getWasmWorkerName(),
93
96
  },
94
97
  );
95
98
 
96
- return getWorkerRPC<SaveWorkerSchema>(saveWorker).proxy.request;
99
+ const rpc = getWorkerRPC<SaveWorkerSchema>(saveWorker);
100
+ rpc.send.bootstrap(runtimeConfig);
101
+ return rpc.proxy.request;
97
102
  }
98
103
 
99
104
  private constructor() {
@@ -101,20 +106,17 @@ export class PyodideBridge implements RunRequests, EditRequests {
101
106
  return;
102
107
  }
103
108
 
109
+ const runtimeConfig = getWasmRuntimeConfig();
110
+
104
111
  // Create a worker
105
- const worker = new Worker(
106
- // oxlint-disable-next-line unicorn/relative-url-style
107
- new URL("./worker/worker.ts", import.meta.url),
108
- {
109
- type: "module",
110
- // Pass the version (and optional capability suffix) to the worker
111
- /* @vite-ignore */
112
- name: getWasmWorkerName(),
113
- },
114
- );
112
+ const worker = createModuleWorker(new URL(workerUrl, import.meta.url), {
113
+ // Pass the version (and optional capability suffix) to the worker
114
+ name: getWasmWorkerName(),
115
+ });
115
116
 
116
117
  // Create the RPC
117
118
  this.rpc = getWorkerRPC<WorkerSchema>(worker);
119
+ this.rpc.send.bootstrap(runtimeConfig);
118
120
 
119
121
  // Listeners
120
122
  this.rpc.addMessageListener("ready", () => {
@@ -6,11 +6,13 @@ import {
6
6
  type RPCSchema,
7
7
  } from "rpc-anywhere";
8
8
  import { Logger } from "@/utils/Logger";
9
+ import type { WasmRuntimeConfig } from "./runtime-config";
9
10
  import { TRANSPORT_ID } from "./worker/constants";
10
11
 
11
12
  export type ParentSchema = RPCSchema<{
12
13
  messages: {
13
14
  consumerReady: {};
15
+ bootstrap: WasmRuntimeConfig;
14
16
  };
15
17
  }>;
16
18
 
@@ -0,0 +1,27 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ export interface WasmRuntimeConfig {
4
+ pyodideIndexUrl?: string;
5
+ pyodideLockfileUrl?: string;
6
+ pypiIndexUrl?: string;
7
+ }
8
+
9
+ export function getWasmRuntimeConfig(): WasmRuntimeConfig {
10
+ const element = document.querySelector<HTMLElement>("marimo-wasm");
11
+ const resolve = (url: string | undefined) => {
12
+ try {
13
+ return url ? new URL(url, document.baseURI).href : undefined;
14
+ } catch {
15
+ return undefined;
16
+ }
17
+ };
18
+ return {
19
+ pyodideIndexUrl: resolve(element?.dataset.pyodideIndexUrl),
20
+ pyodideLockfileUrl: resolve(element?.dataset.pyodideLockfileUrl),
21
+ // URL serialization escapes the placeholder understood by micropip.
22
+ pypiIndexUrl: resolve(element?.dataset.pypiIndexUrl)?.replaceAll(
23
+ "%7Bpackage_name%7D",
24
+ "{package_name}",
25
+ ),
26
+ };
27
+ }
@@ -41,10 +41,9 @@ export class DefaultWasmController implements WasmController {
41
41
  return this.pyodide;
42
42
  }
43
43
 
44
- async bootstrap(opts: {
45
- version: string;
46
- pyodideVersion: string;
47
- }): Promise<PyodideInterface> {
44
+ async bootstrap(
45
+ opts: Parameters<WasmController["bootstrap"]>[0],
46
+ ): Promise<PyodideInterface> {
48
47
  const pyodide = await this.loadPyodideAndPackages(opts);
49
48
 
50
49
  if (MAKE_SNAPSHOT) {
@@ -55,10 +54,9 @@ export class DefaultWasmController implements WasmController {
55
54
  return pyodide;
56
55
  }
57
56
 
58
- private async loadPyodideAndPackages(opts: {
59
- version: string;
60
- pyodideVersion: string;
61
- }): Promise<PyodideInterface> {
57
+ private async loadPyodideAndPackages(
58
+ opts: Parameters<WasmController["bootstrap"]>[0],
59
+ ): Promise<PyodideInterface> {
62
60
  if (!loadPyodide) {
63
61
  throw new Error("loadPyodide is not defined");
64
62
  }
@@ -68,7 +66,9 @@ export class DefaultWasmController implements WasmController {
68
66
  // Without this, this fails in Firefox with
69
67
  // `Could not extract indexURL path from pyodide module`
70
68
  // This fixes for Firefox and does not break Chrome/others
71
- const indexURL = `https://cdn.jsdelivr.net/pyodide/${opts.pyodideVersion}/full/`;
69
+ const indexURL =
70
+ opts.pyodideIndexUrl ??
71
+ `https://cdn.jsdelivr.net/pyodide/${opts.pyodideVersion}/full/`;
72
72
  const pyodide = await loadPyodide({
73
73
  // Perf: These get loaded while pyodide is being bootstrapped
74
74
  packages: [
@@ -81,16 +81,26 @@ export class DefaultWasmController implements WasmController {
81
81
  "packaging",
82
82
  ],
83
83
  _makeSnapshot: MAKE_SNAPSHOT,
84
- lockFileURL: `https://wasm.marimo.app/pyodide-lock.json?v=${opts.version}&pyodide=${opts.pyodideVersion}`,
84
+ lockFileURL:
85
+ opts.pyodideLockfileUrl ??
86
+ `https://wasm.marimo.app/pyodide-lock.json?v=${opts.version}&pyodide=${opts.pyodideVersion}`,
85
87
  indexURL,
86
88
  // Since Pyodide 0.28.0, when lockFileURL is set, the package base URL
87
89
  // defaults to the lockfile's URL (wasm.marimo.app) instead of indexURL.
88
90
  // Unlike Node, browsers get no CDN fallback on a failed fetch, so we
89
- // should pin packageBaseUrl back to the jsDelivr CDN to restore
91
+ // should pin packageBaseUrl to indexURL to restore
90
92
  // the resolution akin to pre-0.28.
91
93
  packageBaseUrl: indexURL,
92
94
  convertNullToNone: true,
93
95
  });
96
+ if (opts.pypiIndexUrl) {
97
+ const micropip = pyodide.pyimport("micropip");
98
+ try {
99
+ micropip.set_index_urls(opts.pypiIndexUrl);
100
+ } finally {
101
+ micropip.destroy();
102
+ }
103
+ }
94
104
  this.pyodide = pyodide;
95
105
  span.end("ok");
96
106
  return pyodide;
@@ -20,6 +20,10 @@ export const WasmFileSystem = {
20
20
  FS.chdir(HOME_DIR);
21
21
  },
22
22
  mountFS: (pyodide: PyodideInterface) => {
23
+ // Opaque origins (single-file exports) use the in-memory filesystem.
24
+ if (globalThis.origin === "null") {
25
+ return;
26
+ }
23
27
  const FS = getFS(pyodide);
24
28
  // Mount the filesystem
25
29
  FS.mount(pyodide.FS.filesystems.IDBFS, { root: "." }, HOME_DIR);
@@ -9,9 +9,11 @@ import {
9
9
  } from "rpc-anywhere";
10
10
  import type { SaveNotebookRequest } from "@/core/network/types";
11
11
  import { decodeUtf8 } from "@/utils/strings";
12
+ import { Deferred } from "../../../utils/Deferred";
12
13
  import { prettyError } from "../../../utils/errors";
13
14
  import { Logger } from "../../../utils/Logger";
14
15
  import type { ParentSchema } from "../rpc";
16
+ import type { WasmRuntimeConfig } from "../runtime-config";
15
17
  import { TRANSPORT_ID } from "./constants";
16
18
  import { WasmFileSystem } from "./fs";
17
19
  import { getController } from "./getController";
@@ -25,6 +27,8 @@ declare const self: Window & {
25
27
  pyodide: PyodideInterface;
26
28
  };
27
29
 
30
+ const runtimeConfig = new Deferred<WasmRuntimeConfig>();
31
+
28
32
  // Initialize
29
33
  async function loadPyodideAndPackages() {
30
34
  try {
@@ -36,6 +40,7 @@ async function loadPyodideAndPackages() {
36
40
  const controller = await getController(marimoVersion);
37
41
  self.controller = controller;
38
42
  self.pyodide = await controller.bootstrap({
43
+ ...(await runtimeConfig.promise),
39
44
  version: marimoVersion,
40
45
  pyodideVersion: pyodideVersion,
41
46
  });
@@ -100,6 +105,8 @@ const rpc = createRPC<SaveWorkerSchema, ParentSchema>({
100
105
  requestHandler,
101
106
  });
102
107
 
108
+ rpc.addMessageListener("bootstrap", (config) => runtimeConfig.resolve(config));
109
+
103
110
  rpc.send("ready", {});
104
111
 
105
112
  function getMarimoVersion() {