@marimo-team/islands 0.23.12-dev9 → 0.23.13-dev0

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 (36) hide show
  1. package/dist/{chat-ui-BEOvjkmJ.js → chat-ui-CsPewo4h.js} +2 -2
  2. package/dist/{code-visibility-w2yZTVwB.js → code-visibility-D9IipVFG.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-CuqTvwmg.js → reveal-component-Dk32fyu2.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/ai/__tests__/completion-utils.test.ts +48 -2
  13. package/src/components/editor/ai/completion-utils.ts +54 -36
  14. package/src/components/editor/app-container.tsx +3 -1
  15. package/src/components/editor/output/ImageOutput.tsx +12 -3
  16. package/src/components/editor/renderers/vertical-layout/vertical-layout-wrapper.tsx +2 -2
  17. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +67 -0
  18. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +47 -0
  19. package/src/core/codemirror/go-to-definition/commands.ts +47 -30
  20. package/src/core/codemirror/go-to-definition/utils.ts +0 -1
  21. package/src/core/codemirror/reactive-references/__tests__/analyzer.test.ts +54 -0
  22. package/src/core/codemirror/reactive-references/analyzer.ts +44 -35
  23. package/src/core/islands/__tests__/bridge.test.ts +25 -0
  24. package/src/core/islands/__tests__/parse.test.ts +585 -1
  25. package/src/core/islands/__tests__/test-utils.tsx +10 -1
  26. package/src/core/islands/bridge.ts +6 -1
  27. package/src/core/islands/constants.ts +2 -0
  28. package/src/core/islands/parse.ts +290 -13
  29. package/src/plugins/impl/DataTablePlugin.tsx +20 -1
  30. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +141 -1
  31. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +54 -4
  32. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +104 -1
  33. package/src/plugins/impl/anywidget/__tests__/model.test.ts +19 -0
  34. package/src/plugins/impl/anywidget/model.ts +15 -0
  35. package/src/utils/__tests__/records.test.ts +27 -0
  36. package/src/utils/records.ts +12 -0
@@ -9,7 +9,8 @@ import { MODEL_MANAGER, Model } from "../model";
9
9
  import type { WidgetModelId } from "../types";
10
10
  import { BINDING_MANAGER } from "../widget-binding";
11
11
 
12
- const { LoadedSlot } = visibleForTesting;
12
+ const { LoadedSlot, isAnyWidgetModule, getInvalidAnyWidgetModuleError } =
13
+ visibleForTesting;
13
14
 
14
15
  // Helper to create typed model IDs for tests
15
16
  const asModelId = (id: string): WidgetModelId => id as WidgetModelId;
@@ -115,4 +116,106 @@ describe("LoadedSlot", () => {
115
116
  expect(newMockWidget.render).toHaveBeenCalled();
116
117
  });
117
118
  });
119
+
120
+ it("should hydrate view state even when listener attaches late", async () => {
121
+ mockModel = new Model(
122
+ { count: 8 },
123
+ {
124
+ sendUpdate: vi.fn().mockResolvedValue(undefined),
125
+ sendCustomMessage: vi.fn().mockResolvedValue(undefined),
126
+ },
127
+ );
128
+ MODEL_MANAGER.set(modelId, mockModel);
129
+
130
+ const lateListenerWidget = {
131
+ initialize: vi.fn(),
132
+ render: vi.fn(({ model, el }) => {
133
+ // Simulate a widget view that starts with a local default and
134
+ // relies on change events for hydration.
135
+ el.textContent = "count is 5";
136
+ const onCount = () => {
137
+ el.textContent = `count is ${model.get("count")}`;
138
+ };
139
+ model.on("change:count", onCount);
140
+ return () => model.off("change:count", onCount);
141
+ }),
142
+ };
143
+
144
+ const { container } = render(
145
+ <LoadedSlot {...mockProps} widget={lateListenerWidget} />,
146
+ );
147
+
148
+ await waitFor(() => {
149
+ expect(lateListenerWidget.render).toHaveBeenCalled();
150
+ expect(container.textContent).toContain("count is 8");
151
+ });
152
+ });
153
+ });
154
+
155
+ describe("isAnyWidgetModule", () => {
156
+ it("should accept a default object with render", () => {
157
+ expect(isAnyWidgetModule({ default: { render: () => undefined } })).toBe(
158
+ true,
159
+ );
160
+ });
161
+
162
+ it("should accept a default factory function", () => {
163
+ expect(
164
+ isAnyWidgetModule({ default: async () => ({ render: () => {} }) }),
165
+ ).toBe(true);
166
+ });
167
+
168
+ it("should reject legacy named render exports", () => {
169
+ expect(isAnyWidgetModule({ render: () => undefined })).toBe(false);
170
+ });
171
+ });
172
+
173
+ describe("getInvalidAnyWidgetModuleError", () => {
174
+ const jsUrl = "./@file/widget.js";
175
+
176
+ it("should explain legacy named render exports", () => {
177
+ const error = getInvalidAnyWidgetModuleError(
178
+ { render: () => undefined },
179
+ jsUrl,
180
+ );
181
+ expect(error.message).toContain("named exports (`render`)");
182
+ expect(error.message).toContain("`export default { render }`");
183
+ expect(error.message).toContain("not `export function render`");
184
+ });
185
+
186
+ it("should explain legacy named initialize exports", () => {
187
+ const error = getInvalidAnyWidgetModuleError(
188
+ { initialize: () => undefined },
189
+ jsUrl,
190
+ );
191
+ expect(error.message).toContain("named exports (`initialize`)");
192
+ expect(error.message).toContain("`export default { initialize }`");
193
+ expect(error.message).toContain("not `export function initialize`");
194
+ });
195
+
196
+ it("should avoid nested backticks for multi-hook named exports", () => {
197
+ const error = getInvalidAnyWidgetModuleError(
198
+ { render: () => undefined, initialize: () => undefined },
199
+ jsUrl,
200
+ );
201
+ expect(error.message).toContain(
202
+ "`export default { render, initialize }` (not `named export function ...`).",
203
+ );
204
+ expect(error.message).not.toContain("`named `export");
205
+ });
206
+
207
+ it("should explain a missing default export", () => {
208
+ expect(getInvalidAnyWidgetModuleError({}, jsUrl).message).toContain(
209
+ "missing a default export",
210
+ );
211
+ expect(getInvalidAnyWidgetModuleError(null, jsUrl).message).toContain(
212
+ "missing a default export",
213
+ );
214
+ });
215
+
216
+ it("should explain an invalid default export", () => {
217
+ const error = getInvalidAnyWidgetModuleError({ default: {} }, jsUrl);
218
+ expect(error.message).toContain("invalid default export");
219
+ expect(error.message).toContain("https://anywidget.dev/en/afm/");
220
+ });
118
221
  });
@@ -272,6 +272,25 @@ describe("Model", () => {
272
272
  });
273
273
  });
274
274
 
275
+ describe("reemitState", () => {
276
+ it("should emit change events for current values without state changes", async () => {
277
+ const onFoo = vi.fn();
278
+ const onBar = vi.fn();
279
+ const onAny = vi.fn();
280
+
281
+ model.on("change:foo", onFoo);
282
+ model.on("change:bar", onBar);
283
+ model.on("change", onAny);
284
+
285
+ getMarimoInternal(model).reemitState();
286
+ await TestUtils.nextTick();
287
+
288
+ expect(onFoo).toHaveBeenCalledWith("test");
289
+ expect(onBar).toHaveBeenCalledWith(123);
290
+ expect(onAny).toHaveBeenCalledTimes(1);
291
+ });
292
+ });
293
+
275
294
  describe("emitCustomMessage", () => {
276
295
  it("should handle custom messages", () => {
277
296
  const callback = vi.fn();
@@ -112,6 +112,10 @@ interface MarimoInternalApi<T extends ModelState> {
112
112
  * Update model state and emit change events for any differences.
113
113
  */
114
114
  updateAndEmitDiffs: (value: T) => void;
115
+ /**
116
+ * Re-emit current state as change events.
117
+ */
118
+ reemitState: () => void;
115
119
  /**
116
120
  * Emit a custom message to listeners.
117
121
  */
@@ -160,6 +164,7 @@ export class Model<T extends ModelState> implements AnyModel<T> {
160
164
  */
161
165
  [marimoSymbol]: MarimoInternalApi<T> = {
162
166
  updateAndEmitDiffs: (value: T) => this.#updateAndEmitDiffs(value),
167
+ reemitState: () => this.#reemitState(),
163
168
  emitCustomMessage: (
164
169
  message: Extract<AnyWidgetMessage, { method: "custom" }>,
165
170
  buffers?: readonly DataView[],
@@ -269,6 +274,16 @@ export class Model<T extends ModelState> implements AnyModel<T> {
269
274
  });
270
275
  }
271
276
 
277
+ #reemitState() {
278
+ for (const [key, value] of Object.entries(this.#data) as [
279
+ keyof T & string,
280
+ T[keyof T],
281
+ ][]) {
282
+ this.#emit(`change:${key}`, value);
283
+ }
284
+ this.#emitAnyChange();
285
+ }
286
+
272
287
  /**
273
288
  * When receiving a message from the backend.
274
289
  * We want to notify all listeners with `msg:custom`
@@ -0,0 +1,27 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { hasFunctionProperty, isRecord } from "../records";
5
+
6
+ describe("isRecord", () => {
7
+ it("should accept plain objects", () => {
8
+ expect(isRecord({})).toBe(true);
9
+ expect(isRecord({ a: 1 })).toBe(true);
10
+ });
11
+
12
+ it("should reject null, arrays, and primitives", () => {
13
+ expect(isRecord(null)).toBe(false);
14
+ expect(isRecord([])).toBe(false);
15
+ expect(isRecord("x")).toBe(false);
16
+ expect(isRecord(1)).toBe(false);
17
+ });
18
+ });
19
+
20
+ describe("hasFunctionProperty", () => {
21
+ it("should detect function properties", () => {
22
+ expect(hasFunctionProperty({ render: () => undefined }, "render")).toBe(
23
+ true,
24
+ );
25
+ expect(hasFunctionProperty({ render: 1 }, "render")).toBe(false);
26
+ });
27
+ });
@@ -0,0 +1,12 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ export function isRecord(value: unknown): value is Record<string, unknown> {
4
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+
7
+ export function hasFunctionProperty(
8
+ record: Record<string, unknown>,
9
+ key: string,
10
+ ): boolean {
11
+ return typeof record[key] === "function";
12
+ }