@marimo-team/islands 0.23.12-dev8 → 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.
Files changed (41) hide show
  1. package/dist/{chat-ui-BEOvjkmJ.js → chat-ui-CsPewo4h.js} +2 -2
  2. package/dist/{code-visibility-B9yvB9rV.js → code-visibility-BFhOAQbo.js} +714 -707
  3. package/dist/{html-to-image-Di0mtt6O.js → html-to-image-DXwLcQ6l.js} +22 -15
  4. package/dist/main.js +1160 -1027
  5. package/dist/{process-output-BLd4KuwX.js → process-output-C6_e1pT_.js} +1 -1
  6. package/dist/{reveal-component-D6wEWbxH.js → reveal-component-ghVwQgXR.js} +13 -13
  7. package/dist/style.css +1 -1
  8. package/package.json +1 -1
  9. package/src/components/data-table/TableBottomBar.tsx +4 -1
  10. package/src/components/data-table/data-table.tsx +26 -17
  11. package/src/components/data-table/utils.ts +1 -4
  12. package/src/components/editor/actions/useNotebookActions.tsx +4 -4
  13. package/src/components/editor/ai/__tests__/completion-utils.test.ts +48 -2
  14. package/src/components/editor/ai/completion-utils.ts +54 -36
  15. package/src/components/editor/app-container.tsx +3 -1
  16. package/src/components/editor/output/ImageOutput.tsx +12 -3
  17. package/src/components/editor/renderers/vertical-layout/vertical-layout-wrapper.tsx +2 -2
  18. package/src/components/home/components.tsx +4 -4
  19. package/src/components/icons/github.tsx +21 -0
  20. package/src/components/icons/youtube.tsx +21 -0
  21. package/src/components/storage/components.tsx +3 -7
  22. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +67 -0
  23. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +47 -0
  24. package/src/core/codemirror/go-to-definition/commands.ts +47 -30
  25. package/src/core/codemirror/go-to-definition/utils.ts +0 -1
  26. package/src/core/codemirror/reactive-references/__tests__/analyzer.test.ts +54 -0
  27. package/src/core/codemirror/reactive-references/analyzer.ts +44 -35
  28. package/src/core/islands/__tests__/bridge.test.ts +25 -0
  29. package/src/core/islands/__tests__/parse.test.ts +585 -1
  30. package/src/core/islands/__tests__/test-utils.tsx +10 -1
  31. package/src/core/islands/bridge.ts +6 -1
  32. package/src/core/islands/constants.ts +2 -0
  33. package/src/core/islands/parse.ts +290 -13
  34. package/src/plugins/impl/DataTablePlugin.tsx +20 -1
  35. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +141 -1
  36. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +54 -4
  37. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +104 -1
  38. package/src/plugins/impl/anywidget/__tests__/model.test.ts +19 -0
  39. package/src/plugins/impl/anywidget/model.ts +15 -0
  40. package/src/utils/__tests__/records.test.ts +27 -0
  41. package/src/utils/records.ts +12 -0
@@ -1,9 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
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 ? getMarimoExportContext() : undefined;
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);
@@ -10,6 +10,8 @@ export const ISLAND_TAG_NAMES = {
10
10
  CODE_EDITOR: "marimo-code-editor",
11
11
  } as const;
12
12
 
13
+ export const ISLANDS_JSON_SCRIPT_TYPE = "application/vnd.marimo.islands+json";
14
+
13
15
  /**
14
16
  * Data attributes for islands
15
17
  */