@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.
- package/dist/{chat-ui-BEOvjkmJ.js → chat-ui-CsPewo4h.js} +2 -2
- package/dist/{code-visibility-B9yvB9rV.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-D6wEWbxH.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/actions/useNotebookActions.tsx +4 -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/components/home/components.tsx +4 -4
- package/src/components/icons/github.tsx +21 -0
- package/src/components/icons/youtube.tsx +21 -0
- package/src/components/storage/components.tsx +3 -7
- 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
|
@@ -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 } =
|
|
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
|
+
}
|