@marimo-team/islands 0.23.12-dev9 → 0.23.12
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/{chat-ui-BEOvjkmJ.js → chat-ui-CsPewo4h.js} +2 -2
- package/dist/{code-visibility-w2yZTVwB.js → code-visibility-BFhOAQbo.js} +714 -707
- package/dist/{html-to-image-Di0mtt6O.js → html-to-image-DXwLcQ6l.js} +22 -15
- package/dist/main.js +1160 -1027
- package/dist/{process-output-BLd4KuwX.js → process-output-C6_e1pT_.js} +1 -1
- package/dist/{reveal-component-CuqTvwmg.js → reveal-component-ghVwQgXR.js} +13 -13
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/data-table/TableBottomBar.tsx +4 -1
- package/src/components/data-table/data-table.tsx +26 -17
- package/src/components/data-table/utils.ts +1 -4
- package/src/components/editor/ai/__tests__/completion-utils.test.ts +48 -2
- package/src/components/editor/ai/completion-utils.ts +54 -36
- package/src/components/editor/app-container.tsx +3 -1
- package/src/components/editor/output/ImageOutput.tsx +12 -3
- package/src/components/editor/renderers/vertical-layout/vertical-layout-wrapper.tsx +2 -2
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +67 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +47 -0
- package/src/core/codemirror/go-to-definition/commands.ts +47 -30
- package/src/core/codemirror/go-to-definition/utils.ts +0 -1
- package/src/core/codemirror/reactive-references/__tests__/analyzer.test.ts +54 -0
- package/src/core/codemirror/reactive-references/analyzer.ts +44 -35
- package/src/core/islands/__tests__/bridge.test.ts +25 -0
- package/src/core/islands/__tests__/parse.test.ts +585 -1
- package/src/core/islands/__tests__/test-utils.tsx +10 -1
- package/src/core/islands/bridge.ts +6 -1
- package/src/core/islands/constants.ts +2 -0
- package/src/core/islands/parse.ts +290 -13
- package/src/plugins/impl/DataTablePlugin.tsx +20 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +141 -1
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +54 -4
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +104 -1
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +19 -0
- package/src/plugins/impl/anywidget/model.ts +15 -0
- package/src/utils/__tests__/records.test.ts +27 -0
- package/src/utils/records.ts +12 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
4
|
import {
|
|
4
5
|
ISLAND_DATA_ATTRIBUTES,
|
|
5
6
|
ISLAND_TAG_NAMES,
|
|
7
|
+
ISLANDS_JSON_SCRIPT_TYPE,
|
|
6
8
|
} from "@/core/islands/constants";
|
|
9
|
+
import { Logger } from "@/utils/Logger";
|
|
7
10
|
import {
|
|
8
11
|
createMarimoFile,
|
|
9
12
|
extractIslandCodeFromEmbed,
|
|
@@ -15,6 +18,43 @@ import {
|
|
|
15
18
|
} from "../parse";
|
|
16
19
|
import { createMockIslandElement, createMockIslands } from "./test-utils.tsx";
|
|
17
20
|
|
|
21
|
+
function createPayloadCell(
|
|
22
|
+
overrides: Partial<{
|
|
23
|
+
cellId: string;
|
|
24
|
+
code: string;
|
|
25
|
+
outputHtml: string;
|
|
26
|
+
outputMimetype: string;
|
|
27
|
+
reactive: boolean;
|
|
28
|
+
displayCode: boolean;
|
|
29
|
+
displayOutput: boolean;
|
|
30
|
+
}> = {},
|
|
31
|
+
) {
|
|
32
|
+
return {
|
|
33
|
+
cellId: "cell-1",
|
|
34
|
+
code: 'print("payload")',
|
|
35
|
+
outputHtml: "<div>payload</div>",
|
|
36
|
+
outputMimetype: "text/html",
|
|
37
|
+
reactive: true,
|
|
38
|
+
displayCode: false,
|
|
39
|
+
displayOutput: true,
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function appendPayload(
|
|
45
|
+
root: HTMLElement,
|
|
46
|
+
payload: {
|
|
47
|
+
schemaVersion: number;
|
|
48
|
+
appId: string;
|
|
49
|
+
cells: ReturnType<typeof createPayloadCell>[];
|
|
50
|
+
},
|
|
51
|
+
) {
|
|
52
|
+
const script = document.createElement("script");
|
|
53
|
+
script.type = ISLANDS_JSON_SCRIPT_TYPE;
|
|
54
|
+
script.textContent = JSON.stringify(payload);
|
|
55
|
+
root.appendChild(script);
|
|
56
|
+
}
|
|
57
|
+
|
|
18
58
|
describe("createMarimoFile", () => {
|
|
19
59
|
it("should return a string", () => {
|
|
20
60
|
const app = {
|
|
@@ -90,6 +130,27 @@ describe("createMarimoFile", () => {
|
|
|
90
130
|
`);
|
|
91
131
|
});
|
|
92
132
|
|
|
133
|
+
it("should create disabled marimo cells", () => {
|
|
134
|
+
const app = {
|
|
135
|
+
cells: [{ code: "x = 0", disabled: true }, { code: "x = 1" }],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const result = createMarimoFile(app);
|
|
139
|
+
|
|
140
|
+
expect(result).toMatchInlineSnapshot(`
|
|
141
|
+
"import marimo
|
|
142
|
+
app = marimo.App()
|
|
143
|
+
@app.cell(disabled=True)
|
|
144
|
+
def __():
|
|
145
|
+
pass
|
|
146
|
+
return
|
|
147
|
+
@app.cell
|
|
148
|
+
def __():
|
|
149
|
+
x = 1
|
|
150
|
+
return"
|
|
151
|
+
`);
|
|
152
|
+
});
|
|
153
|
+
|
|
93
154
|
it("should properly indent multi-line code", () => {
|
|
94
155
|
const app = {
|
|
95
156
|
cells: [{ code: "if True:\n print('hello')\n print('world')" }],
|
|
@@ -459,6 +520,7 @@ describe("parseMarimoIslandApps", () => {
|
|
|
459
520
|
|
|
460
521
|
afterEach(() => {
|
|
461
522
|
document.body.removeChild(container);
|
|
523
|
+
vi.restoreAllMocks();
|
|
462
524
|
});
|
|
463
525
|
|
|
464
526
|
it("should parse islands from document", () => {
|
|
@@ -528,4 +590,526 @@ describe("parseMarimoIslandApps", () => {
|
|
|
528
590
|
]
|
|
529
591
|
`);
|
|
530
592
|
});
|
|
593
|
+
|
|
594
|
+
it("should use supported JSON payload data for matched islands", () => {
|
|
595
|
+
const island = createMockIslandElement({
|
|
596
|
+
appId: "app1",
|
|
597
|
+
cellId: "cell-1",
|
|
598
|
+
code: 'print("dom")',
|
|
599
|
+
innerHTML: "<div>dom</div>",
|
|
600
|
+
});
|
|
601
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
602
|
+
container.appendChild(island);
|
|
603
|
+
appendPayload(container, {
|
|
604
|
+
schemaVersion: 1,
|
|
605
|
+
appId: "app1",
|
|
606
|
+
cells: [createPayloadCell()],
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const result = parseMarimoIslandApps(container);
|
|
610
|
+
|
|
611
|
+
expect(result).toEqual([
|
|
612
|
+
{
|
|
613
|
+
id: "app1",
|
|
614
|
+
payloadBacked: true,
|
|
615
|
+
cells: [
|
|
616
|
+
{
|
|
617
|
+
cellId: "cell-1",
|
|
618
|
+
code: 'print("payload")',
|
|
619
|
+
idx: 0,
|
|
620
|
+
output: "<div>payload</div>",
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
]);
|
|
625
|
+
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("0");
|
|
626
|
+
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID)).toBeNull();
|
|
627
|
+
expect(extractIslandCodeFromEmbed(island)).toBe('print("payload")');
|
|
628
|
+
expect(island.querySelector(ISLAND_TAG_NAMES.CELL_OUTPUT)?.innerHTML).toBe(
|
|
629
|
+
"<div>payload</div>",
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("should parse Python-generated island payload snapshots", () => {
|
|
634
|
+
const html = readFileSync(
|
|
635
|
+
new URL(
|
|
636
|
+
"../../../../../tests/_islands/snapshots/html-payload.txt",
|
|
637
|
+
import.meta.url,
|
|
638
|
+
).pathname.replace(/^\/@fs/, ""),
|
|
639
|
+
"utf8",
|
|
640
|
+
);
|
|
641
|
+
container.innerHTML = html;
|
|
642
|
+
|
|
643
|
+
const result = parseMarimoIslandApps(container);
|
|
644
|
+
const islands = container.querySelectorAll<HTMLElement>(
|
|
645
|
+
ISLAND_TAG_NAMES.ISLAND,
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
expect(result).toEqual([
|
|
649
|
+
{
|
|
650
|
+
id: "main",
|
|
651
|
+
payloadBacked: true,
|
|
652
|
+
cells: [
|
|
653
|
+
{
|
|
654
|
+
cellId: "Hbol",
|
|
655
|
+
code: "import marimo as mo",
|
|
656
|
+
idx: 0,
|
|
657
|
+
output: "",
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
cellId: "MJUe",
|
|
661
|
+
code: "mo.md('Hello, HTML!')",
|
|
662
|
+
idx: 1,
|
|
663
|
+
output: "",
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
},
|
|
667
|
+
]);
|
|
668
|
+
expect(islands[0].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBeNull();
|
|
669
|
+
expect(islands[1].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("0");
|
|
670
|
+
expect(islands[2].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("1");
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it("should fall back to DOM islands for unsupported payload versions", () => {
|
|
674
|
+
const island = createMockIslandElement({
|
|
675
|
+
appId: "app1",
|
|
676
|
+
cellId: "cell-1",
|
|
677
|
+
code: 'print("dom")',
|
|
678
|
+
innerHTML: "<div>dom</div>",
|
|
679
|
+
});
|
|
680
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
681
|
+
container.appendChild(island);
|
|
682
|
+
appendPayload(container, {
|
|
683
|
+
schemaVersion: 2,
|
|
684
|
+
appId: "app1",
|
|
685
|
+
cells: [createPayloadCell()],
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const result = parseMarimoIslandApps(container);
|
|
689
|
+
|
|
690
|
+
expect(result).toEqual([
|
|
691
|
+
{
|
|
692
|
+
id: "app1",
|
|
693
|
+
cells: [
|
|
694
|
+
{
|
|
695
|
+
code: 'print("dom")',
|
|
696
|
+
idx: 0,
|
|
697
|
+
output: "<div>dom</div>",
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
},
|
|
701
|
+
]);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("should ignore payload scripts rendered inside island output", () => {
|
|
705
|
+
const nestedPayload = {
|
|
706
|
+
schemaVersion: 1,
|
|
707
|
+
appId: "app1",
|
|
708
|
+
cells: [
|
|
709
|
+
createPayloadCell({
|
|
710
|
+
code: "payload_code_should_not_run = True",
|
|
711
|
+
outputHtml: "<div>payload</div>",
|
|
712
|
+
}),
|
|
713
|
+
],
|
|
714
|
+
};
|
|
715
|
+
const island = createMockIslandElement({
|
|
716
|
+
appId: "app1",
|
|
717
|
+
cellId: "cell-1",
|
|
718
|
+
code: 'print("dom")',
|
|
719
|
+
innerHTML: `<div>dom</div><script type="${ISLANDS_JSON_SCRIPT_TYPE}">${JSON.stringify(nestedPayload)}</script>`,
|
|
720
|
+
});
|
|
721
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
722
|
+
container.appendChild(island);
|
|
723
|
+
|
|
724
|
+
const result = parseMarimoIslandApps(container);
|
|
725
|
+
|
|
726
|
+
expect(result).toEqual([
|
|
727
|
+
{
|
|
728
|
+
id: "app1",
|
|
729
|
+
cells: [
|
|
730
|
+
{
|
|
731
|
+
code: 'print("dom")',
|
|
732
|
+
idx: 0,
|
|
733
|
+
output: island.querySelector(ISLAND_TAG_NAMES.CELL_OUTPUT)
|
|
734
|
+
?.innerHTML,
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
},
|
|
738
|
+
]);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it("should use payload order for runtime cell indices", () => {
|
|
742
|
+
const second = createMockIslandElement({
|
|
743
|
+
appId: "app1",
|
|
744
|
+
cellId: "cell-2",
|
|
745
|
+
code: "second_dom = True",
|
|
746
|
+
innerHTML: "<div>second dom</div>",
|
|
747
|
+
});
|
|
748
|
+
const first = createMockIslandElement({
|
|
749
|
+
appId: "app1",
|
|
750
|
+
cellId: "cell-1",
|
|
751
|
+
code: "first_dom = True",
|
|
752
|
+
innerHTML: "<div>first dom</div>",
|
|
753
|
+
});
|
|
754
|
+
for (const island of [second, first]) {
|
|
755
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
756
|
+
container.appendChild(island);
|
|
757
|
+
}
|
|
758
|
+
appendPayload(container, {
|
|
759
|
+
schemaVersion: 1,
|
|
760
|
+
appId: "app1",
|
|
761
|
+
cells: [
|
|
762
|
+
createPayloadCell({
|
|
763
|
+
cellId: "cell-1",
|
|
764
|
+
code: "first_payload = True",
|
|
765
|
+
outputHtml: "<div>first payload</div>",
|
|
766
|
+
}),
|
|
767
|
+
createPayloadCell({
|
|
768
|
+
cellId: "cell-2",
|
|
769
|
+
code: "second_payload = True",
|
|
770
|
+
outputHtml: "<div>second payload</div>",
|
|
771
|
+
}),
|
|
772
|
+
],
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
const result = parseMarimoIslandApps(container);
|
|
776
|
+
|
|
777
|
+
expect(result[0].cells.map((cell) => cell.code)).toEqual([
|
|
778
|
+
"first_payload = True",
|
|
779
|
+
"second_payload = True",
|
|
780
|
+
]);
|
|
781
|
+
expect(first.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("0");
|
|
782
|
+
expect(second.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("1");
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it("should include payload-only runtime cells", () => {
|
|
786
|
+
const island = createMockIslandElement({
|
|
787
|
+
appId: "app1",
|
|
788
|
+
cellId: "cell-2",
|
|
789
|
+
code: "value",
|
|
790
|
+
innerHTML: "<div>value</div>",
|
|
791
|
+
});
|
|
792
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
793
|
+
container.appendChild(island);
|
|
794
|
+
appendPayload(container, {
|
|
795
|
+
schemaVersion: 1,
|
|
796
|
+
appId: "app1",
|
|
797
|
+
cells: [
|
|
798
|
+
createPayloadCell({
|
|
799
|
+
cellId: "cell-1",
|
|
800
|
+
code: "import marimo as mo",
|
|
801
|
+
outputHtml: "",
|
|
802
|
+
}),
|
|
803
|
+
createPayloadCell({
|
|
804
|
+
cellId: "cell-2",
|
|
805
|
+
code: "mo.md('visible')",
|
|
806
|
+
outputHtml: "<div>visible</div>",
|
|
807
|
+
}),
|
|
808
|
+
],
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
const result = parseMarimoIslandApps(container);
|
|
812
|
+
|
|
813
|
+
expect(result[0].cells.map((cell) => cell.code)).toEqual([
|
|
814
|
+
"import marimo as mo",
|
|
815
|
+
"mo.md('visible')",
|
|
816
|
+
]);
|
|
817
|
+
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("1");
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("should bind payload-backed islands by runtime index after static cells", () => {
|
|
821
|
+
const island = createMockIslandElement({
|
|
822
|
+
appId: "app1",
|
|
823
|
+
cellId: "cell-2",
|
|
824
|
+
code: "visible_dom = True",
|
|
825
|
+
innerHTML: "<div>visible dom</div>",
|
|
826
|
+
});
|
|
827
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
828
|
+
container.appendChild(island);
|
|
829
|
+
appendPayload(container, {
|
|
830
|
+
schemaVersion: 1,
|
|
831
|
+
appId: "app1",
|
|
832
|
+
cells: [
|
|
833
|
+
createPayloadCell({
|
|
834
|
+
cellId: "cell-1",
|
|
835
|
+
code: "",
|
|
836
|
+
outputHtml: "<div>static</div>",
|
|
837
|
+
reactive: false,
|
|
838
|
+
}),
|
|
839
|
+
createPayloadCell({
|
|
840
|
+
cellId: "cell-2",
|
|
841
|
+
code: "visible_payload = True",
|
|
842
|
+
outputHtml: "<div>visible payload</div>",
|
|
843
|
+
}),
|
|
844
|
+
],
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
const result = parseMarimoIslandApps(container);
|
|
848
|
+
|
|
849
|
+
expect(result[0].cells).toEqual([
|
|
850
|
+
{
|
|
851
|
+
cellId: "cell-1",
|
|
852
|
+
code: "",
|
|
853
|
+
disabled: true,
|
|
854
|
+
idx: 0,
|
|
855
|
+
output: "<div>static</div>",
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
cellId: "cell-2",
|
|
859
|
+
code: "visible_payload = True",
|
|
860
|
+
idx: 1,
|
|
861
|
+
output: "<div>visible payload</div>",
|
|
862
|
+
},
|
|
863
|
+
]);
|
|
864
|
+
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("1");
|
|
865
|
+
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID)).toBeNull();
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it("should fall back to DOM when no payload cell matches an island", () => {
|
|
869
|
+
const island = createMockIslandElement({
|
|
870
|
+
appId: "app1",
|
|
871
|
+
cellId: "dom-only",
|
|
872
|
+
code: "dom_only = True",
|
|
873
|
+
innerHTML: "<div>dom only</div>",
|
|
874
|
+
});
|
|
875
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
876
|
+
island.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX);
|
|
877
|
+
container.appendChild(island);
|
|
878
|
+
appendPayload(container, {
|
|
879
|
+
schemaVersion: 1,
|
|
880
|
+
appId: "app1",
|
|
881
|
+
cells: [
|
|
882
|
+
createPayloadCell({
|
|
883
|
+
cellId: "payload-only",
|
|
884
|
+
code: "payload_only = True",
|
|
885
|
+
outputHtml: "<div>payload only</div>",
|
|
886
|
+
}),
|
|
887
|
+
],
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
const result = parseMarimoIslandApps(container);
|
|
891
|
+
|
|
892
|
+
expect(result).toEqual([
|
|
893
|
+
{
|
|
894
|
+
id: "app1",
|
|
895
|
+
cells: [
|
|
896
|
+
{
|
|
897
|
+
code: "dom_only = True",
|
|
898
|
+
idx: 0,
|
|
899
|
+
output: "<div>dom only</div>",
|
|
900
|
+
},
|
|
901
|
+
],
|
|
902
|
+
},
|
|
903
|
+
]);
|
|
904
|
+
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("0");
|
|
905
|
+
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID)).toBe(
|
|
906
|
+
"dom-only",
|
|
907
|
+
);
|
|
908
|
+
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE)).toBe("true");
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it("should ignore payloads without matching islands", () => {
|
|
912
|
+
const warn = vi.spyOn(Logger, "warn").mockImplementation(() => undefined);
|
|
913
|
+
appendPayload(container, {
|
|
914
|
+
schemaVersion: 1,
|
|
915
|
+
appId: "app1",
|
|
916
|
+
cells: [createPayloadCell()],
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const result = parseMarimoIslandApps(container);
|
|
920
|
+
|
|
921
|
+
expect(result).toEqual([]);
|
|
922
|
+
expect(warn).toHaveBeenCalledWith("No embedded marimo apps found.");
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it("should still parse DOM-only apps when another app has payload", () => {
|
|
926
|
+
const payloadIsland = createMockIslandElement({
|
|
927
|
+
appId: "app1",
|
|
928
|
+
cellId: "cell-1",
|
|
929
|
+
code: "payload_dom = True",
|
|
930
|
+
innerHTML: "<div>payload dom</div>",
|
|
931
|
+
});
|
|
932
|
+
const domIsland = createMockIslandElement({
|
|
933
|
+
appId: "app2",
|
|
934
|
+
code: "dom_app = True",
|
|
935
|
+
innerHTML: "<div>dom app</div>",
|
|
936
|
+
});
|
|
937
|
+
for (const island of [payloadIsland, domIsland]) {
|
|
938
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
939
|
+
container.appendChild(island);
|
|
940
|
+
}
|
|
941
|
+
appendPayload(container, {
|
|
942
|
+
schemaVersion: 1,
|
|
943
|
+
appId: "app1",
|
|
944
|
+
cells: [createPayloadCell()],
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const result = parseMarimoIslandApps(container);
|
|
948
|
+
|
|
949
|
+
expect(result.map((app) => app.id)).toEqual(["app1", "app2"]);
|
|
950
|
+
expect(result[0].cells[0].code).toBe('print("payload")');
|
|
951
|
+
expect(result[1].cells[0].code).toBe("dom_app = True");
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
it("should materialize non-reactive payload islands without runtime cells", () => {
|
|
955
|
+
const island = createMockIslandElement({
|
|
956
|
+
appId: "app1",
|
|
957
|
+
cellId: "cell-1",
|
|
958
|
+
code: "static_dom = True",
|
|
959
|
+
innerHTML: "<div>static dom</div>",
|
|
960
|
+
});
|
|
961
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "false");
|
|
962
|
+
island.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX);
|
|
963
|
+
container.appendChild(island);
|
|
964
|
+
appendPayload(container, {
|
|
965
|
+
schemaVersion: 1,
|
|
966
|
+
appId: "app1",
|
|
967
|
+
cells: [
|
|
968
|
+
createPayloadCell({
|
|
969
|
+
code: "",
|
|
970
|
+
outputHtml: "<div>static payload</div>",
|
|
971
|
+
reactive: false,
|
|
972
|
+
}),
|
|
973
|
+
],
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
const result = parseMarimoIslandApps(container);
|
|
977
|
+
|
|
978
|
+
expect(result).toEqual([]);
|
|
979
|
+
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBeNull();
|
|
980
|
+
expect(island.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID)).toBeNull();
|
|
981
|
+
expect(island.querySelector(ISLAND_TAG_NAMES.CELL_OUTPUT)?.innerHTML).toBe(
|
|
982
|
+
"<div>static payload</div>",
|
|
983
|
+
);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it("should keep non-reactive display code out of runtime cells", () => {
|
|
987
|
+
const staticIsland = createMockIslandElement({
|
|
988
|
+
appId: "app1",
|
|
989
|
+
cellId: "cell-1",
|
|
990
|
+
code: "x = 0",
|
|
991
|
+
innerHTML: "<div>static</div>",
|
|
992
|
+
});
|
|
993
|
+
const reactiveIsland = createMockIslandElement({
|
|
994
|
+
appId: "app1",
|
|
995
|
+
cellId: "cell-2",
|
|
996
|
+
code: "y = 1",
|
|
997
|
+
innerHTML: "<div>reactive</div>",
|
|
998
|
+
});
|
|
999
|
+
staticIsland.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "false");
|
|
1000
|
+
reactiveIsland.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
1001
|
+
container.append(staticIsland, reactiveIsland);
|
|
1002
|
+
appendPayload(container, {
|
|
1003
|
+
schemaVersion: 1,
|
|
1004
|
+
appId: "app1",
|
|
1005
|
+
cells: [
|
|
1006
|
+
createPayloadCell({
|
|
1007
|
+
cellId: "cell-1",
|
|
1008
|
+
code: "x = 0",
|
|
1009
|
+
outputHtml: "<div>static payload</div>",
|
|
1010
|
+
reactive: false,
|
|
1011
|
+
displayCode: true,
|
|
1012
|
+
}),
|
|
1013
|
+
createPayloadCell({
|
|
1014
|
+
cellId: "cell-2",
|
|
1015
|
+
code: "y = 1",
|
|
1016
|
+
outputHtml: "<div>reactive payload</div>",
|
|
1017
|
+
}),
|
|
1018
|
+
],
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const result = parseMarimoIslandApps(container);
|
|
1022
|
+
const file = createMarimoFile(result[0]);
|
|
1023
|
+
|
|
1024
|
+
expect(result[0].cells).toEqual([
|
|
1025
|
+
{
|
|
1026
|
+
cellId: "cell-1",
|
|
1027
|
+
code: "",
|
|
1028
|
+
disabled: true,
|
|
1029
|
+
idx: 0,
|
|
1030
|
+
output: "<div>static payload</div>",
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
cellId: "cell-2",
|
|
1034
|
+
code: "y = 1",
|
|
1035
|
+
idx: 1,
|
|
1036
|
+
output: "<div>reactive payload</div>",
|
|
1037
|
+
},
|
|
1038
|
+
]);
|
|
1039
|
+
expect(file).not.toContain("x = 0");
|
|
1040
|
+
expect(file).toContain("@app.cell(disabled=True)\ndef __():\n pass");
|
|
1041
|
+
expect(reactiveIsland.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe(
|
|
1042
|
+
"1",
|
|
1043
|
+
);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
it("should update editor initial values from payload code", () => {
|
|
1047
|
+
const island = createMockIslandElement({
|
|
1048
|
+
appId: "app1",
|
|
1049
|
+
cellId: "cell-1",
|
|
1050
|
+
code: 'print("dom")',
|
|
1051
|
+
innerHTML: "<div>dom</div>",
|
|
1052
|
+
});
|
|
1053
|
+
island.insertAdjacentHTML(
|
|
1054
|
+
"beforeend",
|
|
1055
|
+
'<div data-marimo-element><marimo-code-editor data-initial-value="\\"print(\\\\\\"dom\\\\\\")\\""></marimo-code-editor></div>',
|
|
1056
|
+
);
|
|
1057
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
1058
|
+
container.appendChild(island);
|
|
1059
|
+
appendPayload(container, {
|
|
1060
|
+
schemaVersion: 1,
|
|
1061
|
+
appId: "app1",
|
|
1062
|
+
cells: [createPayloadCell()],
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
parseMarimoIslandApps(container);
|
|
1066
|
+
|
|
1067
|
+
expect(
|
|
1068
|
+
island
|
|
1069
|
+
.querySelector(ISLAND_TAG_NAMES.CODE_EDITOR)
|
|
1070
|
+
?.getAttribute("data-initial-value"),
|
|
1071
|
+
).toBe(JSON.stringify('print("payload")'));
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
it("should match duplicate cell ids by occurrence", () => {
|
|
1075
|
+
const first = createMockIslandElement({
|
|
1076
|
+
appId: "app1",
|
|
1077
|
+
cellId: "cell-1",
|
|
1078
|
+
code: "dom_first = True",
|
|
1079
|
+
innerHTML: "<div>dom first</div>",
|
|
1080
|
+
});
|
|
1081
|
+
const second = createMockIslandElement({
|
|
1082
|
+
appId: "app1",
|
|
1083
|
+
cellId: "cell-1",
|
|
1084
|
+
code: "dom_second = True",
|
|
1085
|
+
innerHTML: "<div>dom second</div>",
|
|
1086
|
+
});
|
|
1087
|
+
for (const island of [first, second]) {
|
|
1088
|
+
island.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "true");
|
|
1089
|
+
container.appendChild(island);
|
|
1090
|
+
}
|
|
1091
|
+
appendPayload(container, {
|
|
1092
|
+
schemaVersion: 1,
|
|
1093
|
+
appId: "app1",
|
|
1094
|
+
cells: [
|
|
1095
|
+
createPayloadCell({
|
|
1096
|
+
code: "payload_first = True",
|
|
1097
|
+
outputHtml: "<div>payload first</div>",
|
|
1098
|
+
}),
|
|
1099
|
+
createPayloadCell({
|
|
1100
|
+
code: "payload_second = True",
|
|
1101
|
+
outputHtml: "<div>payload second</div>",
|
|
1102
|
+
}),
|
|
1103
|
+
],
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
const result = parseMarimoIslandApps(container);
|
|
1107
|
+
|
|
1108
|
+
expect(result[0].cells.map((cell) => cell.code)).toEqual([
|
|
1109
|
+
"payload_first = True",
|
|
1110
|
+
"payload_second = True",
|
|
1111
|
+
]);
|
|
1112
|
+
expect(first.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("0");
|
|
1113
|
+
expect(second.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("1");
|
|
1114
|
+
});
|
|
531
1115
|
});
|
|
@@ -23,12 +23,14 @@ import type { WorkerFactory } from "@/core/islands/worker-factory";
|
|
|
23
23
|
export function createMockIslandElement(options: {
|
|
24
24
|
appId?: string;
|
|
25
25
|
cellIdx?: string;
|
|
26
|
+
cellId?: string;
|
|
26
27
|
code?: string;
|
|
27
28
|
innerHTML?: string;
|
|
28
29
|
}): HTMLElement {
|
|
29
30
|
const {
|
|
30
31
|
appId = "test-app",
|
|
31
32
|
cellIdx = "0",
|
|
33
|
+
cellId,
|
|
32
34
|
code = "import marimo as mo",
|
|
33
35
|
innerHTML = "",
|
|
34
36
|
} = options;
|
|
@@ -36,6 +38,9 @@ export function createMockIslandElement(options: {
|
|
|
36
38
|
const element = document.createElement(ISLAND_TAG_NAMES.ISLAND);
|
|
37
39
|
element.setAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID, appId);
|
|
38
40
|
element.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, cellIdx);
|
|
41
|
+
if (cellId) {
|
|
42
|
+
element.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID, cellId);
|
|
43
|
+
}
|
|
39
44
|
|
|
40
45
|
if (code) {
|
|
41
46
|
const codeElement = document.createElement(ISLAND_TAG_NAMES.CELL_CODE);
|
|
@@ -146,6 +151,7 @@ export async function waitForNoError<T>(
|
|
|
146
151
|
|
|
147
152
|
export interface IslandSpec {
|
|
148
153
|
appId?: string;
|
|
154
|
+
cellId?: string;
|
|
149
155
|
reactive?: boolean;
|
|
150
156
|
code?: string;
|
|
151
157
|
output?: string;
|
|
@@ -160,6 +166,9 @@ export function buildIslandHTML(islands: IslandSpec[]): string {
|
|
|
160
166
|
return islands
|
|
161
167
|
.map((spec) => {
|
|
162
168
|
const appId = spec.appId ?? "test-app";
|
|
169
|
+
const cellId = spec.cellId
|
|
170
|
+
? ` ${ISLAND_DATA_ATTRIBUTES.CELL_ID}="${spec.cellId}"`
|
|
171
|
+
: "";
|
|
163
172
|
const reactive = spec.reactive ?? true;
|
|
164
173
|
const output = spec.output ?? "<div>output</div>";
|
|
165
174
|
const code = spec.code ?? 'print("hello")';
|
|
@@ -169,7 +178,7 @@ export function buildIslandHTML(islands: IslandSpec[]): string {
|
|
|
169
178
|
: "";
|
|
170
179
|
const outputTag = `<${ISLAND_TAG_NAMES.CELL_OUTPUT}>${output}</${ISLAND_TAG_NAMES.CELL_OUTPUT}>`;
|
|
171
180
|
|
|
172
|
-
return `<${ISLAND_TAG_NAMES.ISLAND} ${ISLAND_DATA_ATTRIBUTES.APP_ID}="${appId}" ${ISLAND_DATA_ATTRIBUTES.REACTIVE}="${reactive}">${outputTag}${codeTag}</${ISLAND_TAG_NAMES.ISLAND}>`;
|
|
181
|
+
return `<${ISLAND_TAG_NAMES.ISLAND} ${ISLAND_DATA_ATTRIBUTES.APP_ID}="${appId}"${cellId} ${ISLAND_DATA_ATTRIBUTES.REACTIVE}="${reactive}">${outputTag}${codeTag}</${ISLAND_TAG_NAMES.ISLAND}>`;
|
|
173
182
|
})
|
|
174
183
|
.join("\n");
|
|
175
184
|
}
|
|
@@ -124,8 +124,13 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
124
124
|
`Starting sessions for ${apps.length} app(s):`,
|
|
125
125
|
apps.map((a) => `${a.id} (${a.cells.length} cells)`),
|
|
126
126
|
);
|
|
127
|
+
// Payload-backed apps already carry the exact runtime cells and order. The
|
|
128
|
+
// full-notebook export context may describe a different source and would
|
|
129
|
+
// override the payload contract for single-app pages.
|
|
127
130
|
const exportContext =
|
|
128
|
-
apps.length === 1
|
|
131
|
+
apps.length === 1 && !apps[0]?.payloadBacked
|
|
132
|
+
? getMarimoExportContext()
|
|
133
|
+
: undefined;
|
|
129
134
|
const notebookCode = exportContext?.notebookCode;
|
|
130
135
|
for (const app of apps) {
|
|
131
136
|
const file = notebookCode || createMarimoFile(app);
|