@marimo-team/islands 0.19.8-dev3 → 0.19.8-dev31
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/{Combination-Bg-xN8JV.js → Combination-BTMrlhzT.js} +11 -10
- package/dist/{ConnectedDataExplorerComponent-DewsKLl2.js → ConnectedDataExplorerComponent-BAeQ8DWw.js} +11 -11
- package/dist/{ImageComparisonComponent-Bijp8beW.js → ImageComparisonComponent-DkEXPki_.js} +2 -2
- package/dist/{any-language-editor-DZc6NCTp.js → any-language-editor-D0UQItkS.js} +6 -6
- package/dist/{architectureDiagram-VXUJARFQ--NkyBn9Y.js → architectureDiagram-VXUJARFQ-DPPYVq8H.js} +4 -4
- package/dist/assets/__vite-browser-external-6-UwTyQC.js +1 -0
- package/dist/assets/{worker-SqntmiwV.js → worker-D3e5wDxM.js} +4 -4
- package/dist/{blockDiagram-VD42YOAC-DEZZaTW0.js → blockDiagram-VD42YOAC-BA5N05Y9.js} +4 -4
- package/dist/{button-BWvsJ2Wr.js → button-Cy0ElmIm.js} +2 -2
- package/dist/{c4Diagram-YG6GDRKO-Bj7hwWCO.js → c4Diagram-YG6GDRKO-DJLzuGJJ.js} +3 -3
- package/dist/{channel-B_QrFrGg.js → channel-Dob5kWXR.js} +1 -1
- package/dist/{check-CM_kewwn.js → check-DkNR52Mm.js} +1 -1
- package/dist/{chunk-5FQGJX7Z-D5VFKHmt.js → chunk-5FQGJX7Z-BEb20Lzt.js} +3 -3
- package/dist/{chunk-ABZYJK2D-SZPYmRzN.js → chunk-ABZYJK2D-BXTC53mt.js} +1 -1
- package/dist/{chunk-ATLVNIR6-BI_WwH1o.js → chunk-ATLVNIR6-BJDjUR_c.js} +1 -1
- package/dist/{chunk-B4BG7PRW-BlI9Gm1l.js → chunk-B4BG7PRW-DzmUUpfH.js} +4 -4
- package/dist/{chunk-DI55MBZ5-BXxemMn5.js → chunk-DI55MBZ5-gTd3J8Tu.js} +4 -4
- package/dist/{chunk-EXTU4WIE-CzWtDV99.js → chunk-EXTU4WIE-DyoOs5QX.js} +1 -1
- package/dist/{chunk-JA3XYJ7Z-DQ-2ARfa.js → chunk-JA3XYJ7Z-BGnAIbOP.js} +2 -2
- package/dist/{chunk-JZLCHNYA-CVfjf2vv.js → chunk-JZLCHNYA-CIRgweVQ.js} +4 -4
- package/dist/{chunk-N4CR4FBY-BCZvQ7Jq.js → chunk-N4CR4FBY-DKSvXAIS.js} +5 -5
- package/dist/{chunk-QN33PNHL-DY_2Q2zl.js → chunk-QN33PNHL-B6zC8BTi.js} +1 -1
- package/dist/{chunk-QXUST7PY-BMCjAVR_.js → chunk-QXUST7PY-C7750n_u.js} +5 -5
- package/dist/{chunk-S3R3BYOJ-Ddu0H4Qa.js → chunk-S3R3BYOJ-CBkH6JZZ.js} +1 -1
- package/dist/{chunk-TZMSLE5B-C2wVlbMl.js → chunk-TZMSLE5B-DObGL7xi.js} +1 -1
- package/dist/{classDiagram-2ON5EDUG-D-g7zbyO.js → classDiagram-2ON5EDUG-B9pkKjjc.js} +9 -9
- package/dist/{classDiagram-v2-WZHVMYZB-C7v5zNRD.js → classDiagram-v2-WZHVMYZB-CRhhA0tV.js} +9 -9
- package/dist/{click-outside-container-BCN5BtVO.js → click-outside-container-DNfggvIW.js} +1 -1
- package/dist/{code-block-37QAKDTI-eUgXqGNG.js → code-block-37QAKDTI-u5kgjqmr.js} +2 -2
- package/dist/{compiler-runtime-DHFVbq0b.js → compiler-runtime-B_OLMU9S.js} +1 -1
- package/dist/{copy-B59Bw3-w.js → copy-DRaXIb_a.js} +3 -3
- package/dist/{dagre-6UL2VRFP-DKIPL74O.js → dagre-6UL2VRFP-C2C2XxsB.js} +6 -6
- package/dist/{data-grid-overlay-editor-COyFwFmE.js → data-grid-overlay-editor-BXqtz1ia.js} +4 -4
- package/dist/{diagram-PSM6KHXK-CVTrAZaP.js → diagram-PSM6KHXK-DHBY-94p.js} +5 -5
- package/dist/{diagram-QEK2KX5R-BqHBzu3x.js → diagram-QEK2KX5R-CgMshOwn.js} +3 -3
- package/dist/{diagram-S2PKOQOG-CJD6owcg.js → diagram-S2PKOQOG-F1KPva3Y.js} +3 -3
- package/dist/{dist-Co5PD8Fb.js → dist-BBYTEAvO.js} +1 -1
- package/dist/{erDiagram-Q2GNP2WA-CqOceSf9.js → erDiagram-Q2GNP2WA-18gGng8V.js} +9 -9
- package/dist/{error-banner-C7KLpECd.js → error-banner-D2zjeN_a.js} +5 -5
- package/dist/{esm-D4WO8J3G.js → esm-CgRNPmz8.js} +6 -6
- package/dist/{flowDiagram-NV44I4VS-K7-DUifo.js → flowDiagram-NV44I4VS-iHFiHYe0.js} +9 -9
- package/dist/{ganttDiagram-JELNMOA3-BwUFY9Nu.js → ganttDiagram-JELNMOA3-D7GixxiF.js} +2 -2
- package/dist/{gitGraphDiagram-NY62KEGX-CjGRtLb1.js → gitGraphDiagram-NY62KEGX-CJFHytRK.js} +2 -2
- package/dist/{glide-data-editor-C3T7HsLi.js → glide-data-editor-BYwb17Bf.js} +13 -13
- package/dist/{infoDiagram-WHAUD3N6-DNhmDn-6.js → infoDiagram-WHAUD3N6-B5Lkh3A9.js} +2 -2
- package/dist/{journeyDiagram-XKPGCS4Q-BOdK47P8.js → journeyDiagram-XKPGCS4Q-CV_9R9iP.js} +2 -2
- package/dist/{kanban-definition-3W4ZIXB7-A0JC9d0g.js → kanban-definition-3W4ZIXB7-Dp21D5Ym.js} +6 -6
- package/dist/{katex-DJyOeQ91.js → katex-CX2BKujk.js} +1 -1
- package/dist/{katex-Dm9nZf6A.js → katex-Db0k5oV_.js} +1 -1
- package/dist/{label-C4PtQcza.js → label-CxU5JNBW.js} +6 -6
- package/dist/main.js +282 -193
- package/dist/mermaid-4DMBBIKO-BhDCqnO1.js +6 -0
- package/dist/{mermaid-Bqp2Xw99.js → mermaid-B__BZSXU.js} +39 -39
- package/dist/{mhchem-BqdXeZVX.js → mhchem-w1tkUnWr.js} +1 -1
- package/dist/{mindmap-definition-VGOIOE7T-CS6nKN_L.js → mindmap-definition-VGOIOE7T-B_5mfdYp.js} +8 -8
- package/dist/{number-overlay-editor-Bz_bDJQb.js → number-overlay-editor-D-4WQAGX.js} +2 -2
- package/dist/{pieDiagram-ADFJNKIX-DSa60Grk.js → pieDiagram-ADFJNKIX-B-DGEopK.js} +3 -3
- package/dist/{quadrantDiagram-AYHSOK5B-CFnMbP2J.js → quadrantDiagram-AYHSOK5B-M_yRSIZn.js} +1 -1
- package/dist/{react-DdA8EBol.js → react-Bs6Z0kvn.js} +1 -1
- package/dist/{react-dom-DJW8xUDg.js → react-dom-CqtLRVZP.js} +2 -2
- package/dist/{react-plotly-jVjTu07w.js → react-plotly-BuRa9xtI.js} +1 -1
- package/dist/{react-vega-DgHpnZ04.js → react-vega-3WcLHYC7.js} +2 -2
- package/dist/{react-vega-CjiPWyw0.js → react-vega-DLFvGrpJ.js} +1 -1
- package/dist/{requirementDiagram-UZGBJVZJ-ytLQrFTk.js → requirementDiagram-UZGBJVZJ-9Wt82hOZ.js} +8 -8
- package/dist/{sankeyDiagram-TZEHDZUN-KQqXDoky.js → sankeyDiagram-TZEHDZUN-x_aTXZeN.js} +1 -1
- package/dist/{sequenceDiagram-WL72ISMW-ByLI04T5.js → sequenceDiagram-WL72ISMW-CXXmJqiQ.js} +3 -3
- package/dist/{slides-component-BVjvNo92.js → slides-component-Dp-y50K9.js} +4 -4
- package/dist/{spec-Dmb1KfK3.js → spec-HoYHAQo2.js} +6 -6
- package/dist/{stateDiagram-FKZM4ZOC-Dfz8vBbP.js → stateDiagram-FKZM4ZOC-CiSKS_Mx.js} +9 -9
- package/dist/{stateDiagram-v2-4FDKWEC3-DRYoLdT5.js → stateDiagram-v2-4FDKWEC3-A43Itnjp.js} +9 -9
- package/dist/style.css +1 -1
- package/dist/{timeline-definition-IT6M3QCI-CO48XU1B.js → timeline-definition-IT6M3QCI-DR26eWb4.js} +1 -1
- package/dist/{types-CzEZ3EWT.js → types-Bb-6p8hv.js} +8 -8
- package/dist/{useAsyncData-BjNwqCfS.js → useAsyncData-Dyq3DyOF.js} +3 -3
- package/dist/{useDeepCompareMemoize-CfoxVor3.js → useDeepCompareMemoize-CMGprt3H.js} +5 -5
- package/dist/{useIframeCapabilities-BBO_R0ww.js → useIframeCapabilities-DurI5SJh.js} +2 -2
- package/dist/{useTheme-BYG2SH8J.js → useTheme-SlKl8MlS.js} +5 -6
- package/dist/{vega-component-rDX7xwxH.js → vega-component-DU3aSp4m.js} +10 -10
- package/dist/{xychartDiagram-PRI3JC2R-CUIfjNVD.js → xychartDiagram-PRI3JC2R-BcVxCRox.js} +4 -4
- package/dist/{zod-DITCj31F.js → zod-bjADtMKr.js} +3 -3
- package/package.json +18 -18
- package/src/components/app-config/ai-config.tsx +11 -2
- package/src/components/app-config/user-config-form.tsx +0 -54
- package/src/components/chat/acp/__tests__/state.test.ts +69 -0
- package/src/components/chat/acp/state.ts +6 -6
- package/src/components/chat/chat-panel.tsx +47 -30
- package/src/components/data-table/__tests__/data-table.test.tsx +94 -2
- package/src/components/editor/actions/useCellActionButton.tsx +14 -1
- package/src/components/editor/cell/CreateCellButton.tsx +2 -1
- package/src/components/editor/cell/code/cell-editor.tsx +12 -0
- package/src/components/editor/renderers/cell-array.tsx +2 -1
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -0
- package/src/components/pages/gallery-page.tsx +37 -6
- package/src/core/MarimoApp.tsx +12 -8
- package/src/core/ai/context/providers/file.ts +1 -1
- package/src/core/cells/__tests__/cells.test.ts +120 -0
- package/src/core/cells/cells.ts +14 -0
- package/src/core/codemirror/language/languages/markdown.ts +7 -0
- package/src/core/config/feature-flag.tsx +0 -4
- package/src/core/islands/__tests__/bridge.test.ts +241 -0
- package/src/core/islands/bridge.ts +22 -6
- package/src/core/run-app.tsx +11 -4
- package/src/core/static/__tests__/files.test.ts +195 -1
- package/src/core/static/files.ts +39 -9
- package/src/plugins/core/registerReactComponent.tsx +9 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +164 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +7 -1
- package/src/utils/__tests__/blob.test.ts +3 -3
- package/src/utils/__tests__/mime-types.test.ts +8 -10
- package/src/utils/__tests__/url-parser.test.ts +22 -0
- package/src/utils/blob.ts +14 -27
- package/src/utils/mime-types.ts +5 -5
- package/src/utils/url-parser.ts +1 -1
- package/dist/assets/__vite-browser-external-DRa9CT_O.js +0 -1
- package/dist/mermaid-4DMBBIKO-o3xNphpD.js +0 -6
|
@@ -9,8 +9,6 @@ export interface ExperimentalFeatures {
|
|
|
9
9
|
markdown: boolean; // Used in playground (community cloud)
|
|
10
10
|
wasm_layouts: boolean; // Used in playground (community cloud)
|
|
11
11
|
rtc_v2: boolean;
|
|
12
|
-
performant_table_charts: boolean;
|
|
13
|
-
chat_modes: boolean;
|
|
14
12
|
cache_panel: boolean;
|
|
15
13
|
external_agents: boolean;
|
|
16
14
|
server_side_pdf_export: boolean;
|
|
@@ -21,8 +19,6 @@ const defaultValues: ExperimentalFeatures = {
|
|
|
21
19
|
markdown: true,
|
|
22
20
|
wasm_layouts: false,
|
|
23
21
|
rtc_v2: false,
|
|
24
|
-
performant_table_charts: false,
|
|
25
|
-
chat_modes: false,
|
|
26
22
|
cache_panel: false,
|
|
27
23
|
external_agents: import.meta.env.DEV,
|
|
28
24
|
server_side_pdf_export: true,
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
// Mock browser APIs before any imports
|
|
5
|
+
vi.stubGlobal(
|
|
6
|
+
"Worker",
|
|
7
|
+
vi.fn(() => ({
|
|
8
|
+
addEventListener: vi.fn(),
|
|
9
|
+
postMessage: vi.fn(),
|
|
10
|
+
terminate: vi.fn(),
|
|
11
|
+
})),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Create a mock URL class that works as a constructor
|
|
15
|
+
class MockURL {
|
|
16
|
+
href: string;
|
|
17
|
+
constructor(url: string, base?: string | URL) {
|
|
18
|
+
this.href = base ? `${base}/${url}` : url;
|
|
19
|
+
}
|
|
20
|
+
static createObjectURL = vi.fn(() => "blob:mock-url");
|
|
21
|
+
static revokeObjectURL = vi.fn();
|
|
22
|
+
}
|
|
23
|
+
vi.stubGlobal("URL", MockURL);
|
|
24
|
+
|
|
25
|
+
// Mock the worker RPC before importing the bridge
|
|
26
|
+
const mockBridge = vi.fn();
|
|
27
|
+
const mockLoadPackages = vi.fn();
|
|
28
|
+
|
|
29
|
+
vi.mock("@/core/wasm/rpc", () => ({
|
|
30
|
+
getWorkerRPC: () => ({
|
|
31
|
+
proxy: {
|
|
32
|
+
request: {
|
|
33
|
+
bridge: mockBridge,
|
|
34
|
+
loadPackages: mockLoadPackages,
|
|
35
|
+
startSession: vi.fn(),
|
|
36
|
+
},
|
|
37
|
+
send: {
|
|
38
|
+
consumerReady: vi.fn(),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
addMessageListener: vi.fn(),
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Mock the parse module to avoid DOM dependencies
|
|
46
|
+
vi.mock("../parse", () => ({
|
|
47
|
+
parseMarimoIslandApps: () => [],
|
|
48
|
+
createMarimoFile: vi.fn(),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// Mock uuid to have predictable tokens
|
|
52
|
+
vi.mock("@/utils/uuid", () => ({
|
|
53
|
+
generateUUID: () => "test-uuid-12345",
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// Mock getMarimoVersion
|
|
57
|
+
vi.mock("@/core/meta/globals", () => ({
|
|
58
|
+
getMarimoVersion: () => "0.0.0-test",
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// Mock the jotai store
|
|
62
|
+
vi.mock("@/core/state/jotai", () => ({
|
|
63
|
+
store: {
|
|
64
|
+
set: vi.fn(),
|
|
65
|
+
},
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// Now import the bridge class
|
|
69
|
+
import { IslandsPyodideBridge } from "../bridge";
|
|
70
|
+
|
|
71
|
+
describe("IslandsPyodideBridge", () => {
|
|
72
|
+
let bridge: IslandsPyodideBridge;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
vi.clearAllMocks();
|
|
76
|
+
// Reset the singleton by clearing the window property
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
delete (window as any)._marimo_private_IslandsPyodideBridge;
|
|
79
|
+
// Access the singleton - creates a fresh instance
|
|
80
|
+
bridge = IslandsPyodideBridge.INSTANCE;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
// Clean up singleton
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
delete (window as any)._marimo_private_IslandsPyodideBridge;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("sendComponentValues", () => {
|
|
90
|
+
it("should include type field and token in control request", async () => {
|
|
91
|
+
const request = {
|
|
92
|
+
objectIds: ["Hbol-0"],
|
|
93
|
+
values: [58],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await bridge.sendComponentValues(request);
|
|
97
|
+
|
|
98
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
99
|
+
functionName: "put_control_request",
|
|
100
|
+
payload: {
|
|
101
|
+
type: "update-ui-element",
|
|
102
|
+
objectIds: ["Hbol-0"],
|
|
103
|
+
values: [58],
|
|
104
|
+
token: "test-uuid-12345",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should preserve all request properties", async () => {
|
|
110
|
+
const request = {
|
|
111
|
+
objectIds: ["slider-1", "slider-2"],
|
|
112
|
+
values: [10, 20],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
await bridge.sendComponentValues(request);
|
|
116
|
+
|
|
117
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
118
|
+
functionName: "put_control_request",
|
|
119
|
+
payload: expect.objectContaining({
|
|
120
|
+
type: "update-ui-element",
|
|
121
|
+
objectIds: ["slider-1", "slider-2"],
|
|
122
|
+
values: [10, 20],
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("sendFunctionRequest", () => {
|
|
129
|
+
it("should include type field in control request", async () => {
|
|
130
|
+
const request = {
|
|
131
|
+
functionCallId: "call-123",
|
|
132
|
+
namespace: "test_namespace",
|
|
133
|
+
functionName: "my_function",
|
|
134
|
+
args: { x: 1, y: 2 },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
await bridge.sendFunctionRequest(request);
|
|
138
|
+
|
|
139
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
140
|
+
functionName: "put_control_request",
|
|
141
|
+
payload: {
|
|
142
|
+
type: "invoke-function",
|
|
143
|
+
functionCallId: "call-123",
|
|
144
|
+
namespace: "test_namespace",
|
|
145
|
+
functionName: "my_function",
|
|
146
|
+
args: { x: 1, y: 2 },
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("sendRun", () => {
|
|
153
|
+
it("should include type field in control request", async () => {
|
|
154
|
+
const request = {
|
|
155
|
+
cellIds: ["cell-1", "cell-2"],
|
|
156
|
+
codes: ["print('hello')", "print('world')"],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
await bridge.sendRun(request);
|
|
160
|
+
|
|
161
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
162
|
+
functionName: "put_control_request",
|
|
163
|
+
payload: {
|
|
164
|
+
type: "execute-cells",
|
|
165
|
+
cellIds: ["cell-1", "cell-2"],
|
|
166
|
+
codes: ["print('hello')", "print('world')"],
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should call loadPackages before putControlRequest", async () => {
|
|
172
|
+
const request = {
|
|
173
|
+
cellIds: ["cell-1"],
|
|
174
|
+
codes: ["import pandas"],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
await bridge.sendRun(request);
|
|
178
|
+
|
|
179
|
+
// Verify loadPackages was called with joined codes
|
|
180
|
+
expect(mockLoadPackages).toHaveBeenCalledWith("import pandas");
|
|
181
|
+
|
|
182
|
+
// Verify order: loadPackages should be called before bridge
|
|
183
|
+
const loadPackagesCallOrder =
|
|
184
|
+
mockLoadPackages.mock.invocationCallOrder[0];
|
|
185
|
+
const bridgeCallOrder = mockBridge.mock.invocationCallOrder[0];
|
|
186
|
+
expect(loadPackagesCallOrder).toBeLessThan(bridgeCallOrder);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("sendModelValue", () => {
|
|
191
|
+
it("should include type field in control request", async () => {
|
|
192
|
+
const request = {
|
|
193
|
+
modelId: "widget-1",
|
|
194
|
+
message: {
|
|
195
|
+
state: { value: 42 },
|
|
196
|
+
bufferPaths: [],
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await bridge.sendModelValue(request);
|
|
201
|
+
|
|
202
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
203
|
+
functionName: "put_control_request",
|
|
204
|
+
payload: {
|
|
205
|
+
type: "update-widget-model",
|
|
206
|
+
modelId: "widget-1",
|
|
207
|
+
message: {
|
|
208
|
+
state: { value: 42 },
|
|
209
|
+
bufferPaths: [],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("control request message format", () => {
|
|
217
|
+
it("should always include the type field required by msgspec", async () => {
|
|
218
|
+
// Test all methods to ensure they include the type field
|
|
219
|
+
await bridge.sendComponentValues({ objectIds: [], values: [] });
|
|
220
|
+
await bridge.sendFunctionRequest({
|
|
221
|
+
functionCallId: "",
|
|
222
|
+
namespace: "",
|
|
223
|
+
functionName: "",
|
|
224
|
+
args: {},
|
|
225
|
+
});
|
|
226
|
+
await bridge.sendRun({ cellIds: [], codes: [] });
|
|
227
|
+
await bridge.sendModelValue({
|
|
228
|
+
modelId: "",
|
|
229
|
+
message: { state: {}, bufferPaths: [] },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// All calls should have the type field
|
|
233
|
+
const allCalls = mockBridge.mock.calls;
|
|
234
|
+
for (const call of allCalls) {
|
|
235
|
+
const payload = call[0].payload;
|
|
236
|
+
expect(payload).toHaveProperty("type");
|
|
237
|
+
expect(typeof payload.type).toBe("string");
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -6,7 +6,8 @@ import { Deferred } from "@/utils/Deferred";
|
|
|
6
6
|
import { throwNotImplemented } from "@/utils/functions";
|
|
7
7
|
import type { JsonString } from "@/utils/json/base64";
|
|
8
8
|
import { Logger } from "@/utils/Logger";
|
|
9
|
-
import
|
|
9
|
+
import { generateUUID } from "@/utils/uuid";
|
|
10
|
+
import type { CommandMessage, NotificationPayload } from "../kernel/messages";
|
|
10
11
|
import { getMarimoVersion } from "../meta/globals";
|
|
11
12
|
import type { EditRequests, RunRequests } from "../network/types";
|
|
12
13
|
import { store } from "../state/jotai";
|
|
@@ -104,7 +105,11 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
104
105
|
sendComponentValues: RunRequests["sendComponentValues"] = async (
|
|
105
106
|
request,
|
|
106
107
|
): Promise<null> => {
|
|
107
|
-
await this.putControlRequest(
|
|
108
|
+
await this.putControlRequest({
|
|
109
|
+
type: "update-ui-element",
|
|
110
|
+
...request,
|
|
111
|
+
token: generateUUID(),
|
|
112
|
+
});
|
|
108
113
|
return null;
|
|
109
114
|
};
|
|
110
115
|
|
|
@@ -117,18 +122,27 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
117
122
|
sendFunctionRequest: RunRequests["sendFunctionRequest"] = async (
|
|
118
123
|
request,
|
|
119
124
|
): Promise<null> => {
|
|
120
|
-
await this.putControlRequest(
|
|
125
|
+
await this.putControlRequest({
|
|
126
|
+
type: "invoke-function",
|
|
127
|
+
...request,
|
|
128
|
+
});
|
|
121
129
|
return null;
|
|
122
130
|
};
|
|
123
131
|
|
|
124
132
|
sendRun: EditRequests["sendRun"] = async (request): Promise<null> => {
|
|
125
133
|
await this.rpc.proxy.request.loadPackages(request.codes.join("\n"));
|
|
126
|
-
await this.putControlRequest(
|
|
134
|
+
await this.putControlRequest({
|
|
135
|
+
type: "execute-cells",
|
|
136
|
+
...request,
|
|
137
|
+
});
|
|
127
138
|
return null;
|
|
128
139
|
};
|
|
129
140
|
|
|
130
141
|
sendModelValue: RunRequests["sendModelValue"] = async (request) => {
|
|
131
|
-
await this.putControlRequest(
|
|
142
|
+
await this.putControlRequest({
|
|
143
|
+
type: "update-widget-model",
|
|
144
|
+
...request,
|
|
145
|
+
});
|
|
132
146
|
return null;
|
|
133
147
|
};
|
|
134
148
|
|
|
@@ -187,7 +201,9 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
187
201
|
clearCache = throwNotImplemented;
|
|
188
202
|
getCacheInfo = throwNotImplemented;
|
|
189
203
|
|
|
190
|
-
|
|
204
|
+
// The kernel uses msgspec to parse control requests, which requires a 'type'
|
|
205
|
+
// field for discriminated union deserialization.
|
|
206
|
+
private async putControlRequest(operation: CommandMessage) {
|
|
191
207
|
await this.rpc.proxy.request.bridge({
|
|
192
208
|
functionName: "put_control_request",
|
|
193
209
|
payload: operation,
|
package/src/core/run-app.tsx
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { useAtomValue } from "jotai";
|
|
4
|
+
import { ArrowLeftIcon } from "lucide-react";
|
|
4
5
|
import { useEffect } from "react";
|
|
5
6
|
import { AppContainer } from "@/components/editor/app-container";
|
|
6
7
|
import { AppHeader } from "@/components/editor/header/app-header";
|
|
7
8
|
import { Spinner } from "@/components/icons/spinner";
|
|
9
|
+
import { buttonVariants } from "@/components/ui/button";
|
|
8
10
|
import { DelayMount } from "@/components/utils/delay-mount";
|
|
11
|
+
import { cn } from "@/utils/cn";
|
|
9
12
|
import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
|
|
10
13
|
import { notebookIsRunningAtom, useCellActions } from "./cells/cells";
|
|
11
14
|
import type { AppConfig } from "./config/config-schema";
|
|
@@ -75,15 +78,19 @@ export const RunApp: React.FC<AppProps> = ({ appConfig }) => {
|
|
|
75
78
|
isRunning={isRunning}
|
|
76
79
|
width={appConfig.width}
|
|
77
80
|
>
|
|
78
|
-
<AppHeader connection={connection} className=
|
|
81
|
+
<AppHeader connection={connection} className="sm:pt-8">
|
|
79
82
|
{galleryHref && (
|
|
80
|
-
<div className="flex items-center px-6 pt-4">
|
|
83
|
+
<div className="flex items-center px-6 pt-4 sm:-mt-8">
|
|
81
84
|
<a
|
|
82
85
|
href={galleryHref}
|
|
83
86
|
aria-label="Back to gallery"
|
|
84
|
-
className=
|
|
87
|
+
className={cn(
|
|
88
|
+
buttonVariants({ variant: "text", size: "sm" }),
|
|
89
|
+
"gap-2 px-0 text-muted-foreground hover:text-foreground",
|
|
90
|
+
)}
|
|
85
91
|
>
|
|
86
|
-
<
|
|
92
|
+
<ArrowLeftIcon className="size-4" aria-hidden={true} />
|
|
93
|
+
<span>Back</span>
|
|
87
94
|
</a>
|
|
88
95
|
</div>
|
|
89
96
|
)}
|
|
@@ -6,7 +6,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
|
6
6
|
import { createLoader } from "@/plugins/impl/vega/vega-loader";
|
|
7
7
|
import { Functions } from "@/utils/functions";
|
|
8
8
|
import type { DataURLString } from "@/utils/json/base64";
|
|
9
|
-
import { patchFetch, patchVegaLoader } from "../files";
|
|
9
|
+
import { patchFetch, patchVegaLoader, resolveVirtualFileURL } from "../files";
|
|
10
10
|
|
|
11
11
|
// Start a tiny server to serve virtual files
|
|
12
12
|
const server = http.createServer((request, response) => {
|
|
@@ -350,6 +350,181 @@ describe("patchVegaLoader - loader.load", () => {
|
|
|
350
350
|
});
|
|
351
351
|
});
|
|
352
352
|
|
|
353
|
+
describe("resolveVirtualFileURL", () => {
|
|
354
|
+
// Mock URL.createObjectURL for jsdom environment
|
|
355
|
+
const mockBlobURLs = new Map<string, Blob>();
|
|
356
|
+
let blobCounter = 0;
|
|
357
|
+
|
|
358
|
+
beforeAll(() => {
|
|
359
|
+
URL.createObjectURL = vi.fn((blob: Blob) => {
|
|
360
|
+
const url = `blob:test-${blobCounter++}`;
|
|
361
|
+
mockBlobURLs.set(url, blob);
|
|
362
|
+
return url;
|
|
363
|
+
});
|
|
364
|
+
URL.revokeObjectURL = vi.fn((url: string) => {
|
|
365
|
+
mockBlobURLs.delete(url);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
afterAll(() => {
|
|
370
|
+
mockBlobURLs.clear();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should return a blob URL for virtual files", () => {
|
|
374
|
+
const virtualFiles = {
|
|
375
|
+
"/@file/widget.js":
|
|
376
|
+
"data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQgeyByZW5kZXI6ICgpID0+IHt9IH0=" as DataURLString,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const result = resolveVirtualFileURL("/@file/widget.js", virtualFiles);
|
|
380
|
+
|
|
381
|
+
expect(result).toMatch(/^blob:/);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should return the original URL for non-virtual files", () => {
|
|
385
|
+
const virtualFiles = {};
|
|
386
|
+
|
|
387
|
+
const result = resolveVirtualFileURL(
|
|
388
|
+
"http://example.com/widget.js",
|
|
389
|
+
virtualFiles,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
expect(result).toBe("http://example.com/widget.js");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("should handle various URL formats", () => {
|
|
396
|
+
const virtualFiles = {
|
|
397
|
+
"/@file/module.js":
|
|
398
|
+
"data:text/javascript;base64,Y29uc29sZS5sb2coJ3Rlc3QnKQ==" as DataURLString,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const testUrls = [
|
|
402
|
+
"/@file/module.js",
|
|
403
|
+
"./@file/module.js",
|
|
404
|
+
"http://example.com/@file/module.js",
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
for (const url of testUrls) {
|
|
408
|
+
const result = resolveVirtualFileURL(url, virtualFiles);
|
|
409
|
+
expect(result).toMatch(/^blob:/);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("should create blob URL with correct content", async () => {
|
|
414
|
+
const jsCode = "export default { render: () => {} }";
|
|
415
|
+
const base64Code = btoa(jsCode);
|
|
416
|
+
const virtualFiles = {
|
|
417
|
+
"/@file/test-module.js":
|
|
418
|
+
`data:text/javascript;base64,${base64Code}` as DataURLString,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const blobUrl = resolveVirtualFileURL(
|
|
422
|
+
"/@file/test-module.js",
|
|
423
|
+
virtualFiles,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
expect(blobUrl).toMatch(/^blob:/);
|
|
427
|
+
expect(URL.createObjectURL).toHaveBeenCalled();
|
|
428
|
+
|
|
429
|
+
// Verify blob content through the mock
|
|
430
|
+
const blob = mockBlobURLs.get(blobUrl);
|
|
431
|
+
expect(blob).toBeDefined();
|
|
432
|
+
const text = await blob!.text();
|
|
433
|
+
expect(text).toBe(jsCode);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("should handle file:// URLs with @file/ paths", () => {
|
|
437
|
+
const virtualFiles = {
|
|
438
|
+
"/@file/local-module.js":
|
|
439
|
+
"data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQge30=" as DataURLString,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const result = resolveVirtualFileURL(
|
|
443
|
+
"file:///Users/test/@file/local-module.js",
|
|
444
|
+
virtualFiles,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
expect(result).toMatch(/^blob:/);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("should handle different MIME types", async () => {
|
|
451
|
+
const virtualFiles = {
|
|
452
|
+
"/@file/script.js":
|
|
453
|
+
"data:application/javascript;base64,Y29uc3QgeCA9IDE=" as DataURLString,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const blobUrl = resolveVirtualFileURL("/@file/script.js", virtualFiles);
|
|
457
|
+
|
|
458
|
+
// Should still be a valid blob URL
|
|
459
|
+
expect(blobUrl).toMatch(/^blob:/);
|
|
460
|
+
|
|
461
|
+
// Verify blob content through the mock
|
|
462
|
+
const blob = mockBlobURLs.get(blobUrl);
|
|
463
|
+
expect(blob).toBeDefined();
|
|
464
|
+
const text = await blob!.text();
|
|
465
|
+
expect(text).toBe("const x = 1");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("should handle blob: base URIs correctly", () => {
|
|
469
|
+
// Mock document.baseURI to simulate blob: protocol
|
|
470
|
+
const originalBaseURI = document.baseURI;
|
|
471
|
+
Object.defineProperty(document, "baseURI", {
|
|
472
|
+
value: "blob:https://example.com/uuid",
|
|
473
|
+
configurable: true,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const virtualFiles = {
|
|
477
|
+
"/@file/blob-module.js":
|
|
478
|
+
"data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQge30=" as DataURLString,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const result = resolveVirtualFileURL("/@file/blob-module.js", virtualFiles);
|
|
482
|
+
|
|
483
|
+
expect(result).toMatch(/^blob:/);
|
|
484
|
+
|
|
485
|
+
// Restore original baseURI
|
|
486
|
+
Object.defineProperty(document, "baseURI", {
|
|
487
|
+
value: originalBaseURI,
|
|
488
|
+
configurable: true,
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("should handle data URLs with no explicit MIME type", async () => {
|
|
493
|
+
const virtualFiles = {
|
|
494
|
+
"/@file/generic.bin": "data:;base64,SGVsbG8gV29ybGQ=" as DataURLString,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const blobUrl = resolveVirtualFileURL("/@file/generic.bin", virtualFiles);
|
|
498
|
+
expect(blobUrl).toMatch(/^blob:/);
|
|
499
|
+
|
|
500
|
+
// Verify blob content through the mock
|
|
501
|
+
const blob = mockBlobURLs.get(blobUrl);
|
|
502
|
+
expect(blob).toBeDefined();
|
|
503
|
+
const text = await blob!.text();
|
|
504
|
+
expect(text).toBe("Hello World");
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should match URLs with prefix paths before /@file/", async () => {
|
|
508
|
+
const virtualFiles = {
|
|
509
|
+
"/@file/4263-66-yUGhgQXp.js":
|
|
510
|
+
"data:application/javascript;base64,ZnVuY3Rpb24gcmVuZGVyKCkge30=" as DataURLString,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const blobUrl = resolveVirtualFileURL(
|
|
514
|
+
"https://molab.marimo.app/preview/@file/4263-66-yUGhgQXp.js",
|
|
515
|
+
virtualFiles,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
expect(blobUrl).toMatch(/^blob:/);
|
|
519
|
+
|
|
520
|
+
// Verify blob content through the mock
|
|
521
|
+
const blob = mockBlobURLs.get(blobUrl);
|
|
522
|
+
expect(blob).toBeDefined();
|
|
523
|
+
const text = await blob!.text();
|
|
524
|
+
expect(text).toBe("function render() {}");
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
353
528
|
describe("maybeGetVirtualFile utility function", () => {
|
|
354
529
|
it("should handle URLs without leading dots correctly", async () => {
|
|
355
530
|
const virtualFiles = {
|
|
@@ -370,6 +545,25 @@ describe("maybeGetVirtualFile utility function", () => {
|
|
|
370
545
|
expect(text2).toBe("test");
|
|
371
546
|
});
|
|
372
547
|
|
|
548
|
+
it("should match URLs with prefix paths before /@file/", async () => {
|
|
549
|
+
const virtualFiles = {
|
|
550
|
+
"/@file/4263-66-yUGhgQXp.js":
|
|
551
|
+
"data:application/javascript;base64,ZnVuY3Rpb24gcmVuZGVyKCkge30=" as DataURLString,
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const unpatch = patchFetch(virtualFiles);
|
|
555
|
+
|
|
556
|
+
// Test URL with a prefix path before /@file/
|
|
557
|
+
const response = await window.fetch(
|
|
558
|
+
"https://molab.marimo.app/preview/@file/4263-66-yUGhgQXp.js",
|
|
559
|
+
);
|
|
560
|
+
const text = await response.text();
|
|
561
|
+
|
|
562
|
+
expect(text).toBe("function render() {}");
|
|
563
|
+
|
|
564
|
+
unpatch();
|
|
565
|
+
});
|
|
566
|
+
|
|
373
567
|
it("should handle complex file:// URLs with nested paths", async () => {
|
|
374
568
|
const virtualFiles = {
|
|
375
569
|
"/@file/nested/data.json":
|
package/src/core/static/files.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import type { Loader } from "@/plugins/impl/vega/vega-loader";
|
|
4
|
+
import { deserializeBlob } from "@/utils/blob";
|
|
5
|
+
import type { DataURLString } from "@/utils/json/base64";
|
|
4
6
|
import { Logger } from "@/utils/Logger";
|
|
5
7
|
import { getStaticVirtualFiles } from "./static-state";
|
|
6
8
|
import type { StaticVirtualFiles } from "./types";
|
|
@@ -120,6 +122,24 @@ function withoutLeadingDot(path: string): string {
|
|
|
120
122
|
return path.startsWith(".") ? path.slice(1) : path;
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Resolve a URL to a blob URL if it's a virtual file, for use with dynamic import().
|
|
127
|
+
* Unlike fetch, import() can't be patched, so we need to convert data URLs to blob URLs.
|
|
128
|
+
*
|
|
129
|
+
* @returns The original URL if not a virtual file, or a blob URL if it is
|
|
130
|
+
*/
|
|
131
|
+
export function resolveVirtualFileURL(
|
|
132
|
+
url: string,
|
|
133
|
+
files: StaticVirtualFiles = getStaticVirtualFiles(),
|
|
134
|
+
): string {
|
|
135
|
+
const vfile = maybeGetVirtualFile(url, files);
|
|
136
|
+
if (!vfile) {
|
|
137
|
+
return url;
|
|
138
|
+
}
|
|
139
|
+
const blob = deserializeBlob(vfile as DataURLString);
|
|
140
|
+
return URL.createObjectURL(blob);
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
function maybeGetVirtualFile(
|
|
124
144
|
url: string,
|
|
125
145
|
files: StaticVirtualFiles,
|
|
@@ -130,14 +150,11 @@ function maybeGetVirtualFile(
|
|
|
130
150
|
}
|
|
131
151
|
const pathname = new URL(url, base).pathname;
|
|
132
152
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
url = url.slice(indexOfFile);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
153
|
+
// Extract the /@file/... suffix from the URL or pathname
|
|
154
|
+
// This handles URLs like https://example.com/prefix/@file/foo.js
|
|
155
|
+
// or file:///path/to/@file/foo.js
|
|
156
|
+
const filePathFromUrl = extractFilePath(url);
|
|
157
|
+
const filePathFromPathname = extractFilePath(pathname);
|
|
141
158
|
|
|
142
159
|
// Few variations to grab the URL.
|
|
143
160
|
// This can happen if a static file was open at file:// or https://
|
|
@@ -145,6 +162,19 @@ function maybeGetVirtualFile(
|
|
|
145
162
|
files[url] ||
|
|
146
163
|
files[withoutLeadingDot(url)] ||
|
|
147
164
|
files[pathname] ||
|
|
148
|
-
files[withoutLeadingDot(pathname)]
|
|
165
|
+
files[withoutLeadingDot(pathname)] ||
|
|
166
|
+
(filePathFromUrl && files[filePathFromUrl]) ||
|
|
167
|
+
(filePathFromPathname && files[filePathFromPathname])
|
|
149
168
|
);
|
|
150
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extract the /@file/... path from a URL string
|
|
173
|
+
*/
|
|
174
|
+
function extractFilePath(url: string): string | null {
|
|
175
|
+
const indexOfFile = url.indexOf("/@file/");
|
|
176
|
+
if (indexOfFile !== -1) {
|
|
177
|
+
return url.slice(indexOfFile);
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
@@ -101,10 +101,17 @@ function PluginSlotInternal<T>(
|
|
|
101
101
|
return plugin.validator.safeParse(parseDataset(hostElement));
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
// Incremented on each reset to invalidate memoized function references.
|
|
105
|
+
// This ensures that plugin functions (e.g., search) are re-created when
|
|
106
|
+
// the underlying UI element instance changes (new object-id), even if
|
|
107
|
+
// the element's data attributes haven't changed.
|
|
108
|
+
const [resetNonce, setResetNonce] = useState(0);
|
|
109
|
+
|
|
104
110
|
useImperativeHandle(ref, () => ({
|
|
105
111
|
reset: () => {
|
|
106
112
|
setValue(getInitialValue());
|
|
107
113
|
setParsedResult(plugin.validator.safeParse(parseDataset(hostElement)));
|
|
114
|
+
setResetNonce((n) => n + 1);
|
|
108
115
|
},
|
|
109
116
|
setChildren: (children) => {
|
|
110
117
|
setChildNodes(children);
|
|
@@ -224,7 +231,8 @@ function PluginSlotInternal<T>(
|
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
return methods;
|
|
227
|
-
|
|
234
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
|
+
}, [plugin.functions, hostElement, resetNonce]);
|
|
228
236
|
|
|
229
237
|
// If we failed to parse the initial value, render an error
|
|
230
238
|
if (!parsedResult.success) {
|