@marimo-team/frontend 0.24.1-dev51 → 0.24.1-dev53

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 (25) hide show
  1. package/dist/assets/{add-cell-with-ai-B0RdHenk.js → add-cell-with-ai-hxrzwECc.js} +1 -1
  2. package/dist/assets/{agent-panel-i6F-fC5e.js → agent-panel-B9BFcufg.js} +1 -1
  3. package/dist/assets/{ai-model-dropdown-DjQy2bo-.js → ai-model-dropdown-a4n3JDbf.js} +1 -1
  4. package/dist/assets/{app-config-button-COFLeJCU.js → app-config-button-BQAVAUpN.js} +1 -1
  5. package/dist/assets/{cell-editor-X3xHqmCs.js → cell-editor-DfhJQCKm.js} +1 -1
  6. package/dist/assets/{chat-display-ClG9MRI_.js → chat-display-C7EsuQmC.js} +1 -1
  7. package/dist/assets/{chat-panel-BRoqCSCR.js → chat-panel-DCSD5g-j.js} +1 -1
  8. package/dist/assets/{chat-ui-CNvkb287.js → chat-ui-C3ay3Rdi.js} +1 -1
  9. package/dist/assets/{command-palette-L1LZCmet.js → command-palette-D1IoMI0F.js} +1 -1
  10. package/dist/assets/{edit-page-DOpi9pL6.js → edit-page-B1g7AdLD.js} +4 -4
  11. package/dist/assets/{home-page-Chy6SA41.js → home-page-P1nAvpLK.js} +1 -1
  12. package/dist/assets/{index-CNnOirLv.js → index-BMQJCzfh.js} +3 -3
  13. package/dist/assets/{layout-DmbNDyY3.js → layout-vg4c6imf.js} +2 -2
  14. package/dist/assets/{packages-panel-CYLyGihe.js → packages-panel-DMZtOQYl.js} +1 -1
  15. package/dist/assets/{panels-BMikLSqU.js → panels-DllVd27E.js} +1 -1
  16. package/dist/assets/{reveal-component-6t8SeOsr.js → reveal-component-D9uCWzLS.js} +1 -1
  17. package/dist/assets/{run-page-BIyr61hZ.js → run-page-ah4ptdoG.js} +1 -1
  18. package/dist/assets/{scratchpad-panel-B2KUccqM.js → scratchpad-panel-YOarJKLy.js} +1 -1
  19. package/dist/assets/{skeleton-DZV2_Wnl.js → skeleton-BX017Wzj.js} +1 -1
  20. package/dist/assets/{useNotebookActions-BcAzvJBD.js → useNotebookActions-DKMS8mUE.js} +1 -1
  21. package/dist/index.html +1 -1
  22. package/package.json +4 -2
  23. package/src/plugins/__tests__/plugin-schema.test.ts +343 -0
  24. package/src/plugins/__tests__/plugin-schema.ts +398 -0
  25. package/src/plugins/plugins.ts +1 -1
@@ -0,0 +1,343 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { describe, expect, it } from "vitest";
6
+ import { parse, stringify } from "yaml";
7
+ import { z } from "zod";
8
+ import { LAYOUT_PLUGINS, UI_PLUGINS } from "../plugins";
9
+ import {
10
+ buildPluginSpec,
11
+ type BuildPluginSpecOptions,
12
+ type PluginContract,
13
+ type PluginOpenAPIDocument,
14
+ } from "./plugin-schema";
15
+
16
+ const SCHEMA_PATH = resolve(
17
+ import.meta.dirname,
18
+ "../../../plugins.openapi.yaml",
19
+ );
20
+ const REGENERATE_COMMAND =
21
+ "pnpm --filter @marimo-team/frontend plugins:generate-schema";
22
+
23
+ // Keep this aggregation test-only: production initialization consumes the two
24
+ // registries directly and should not allocate or import schema-generation code.
25
+ const ALL_PLUGINS = [...UI_PLUGINS, ...LAYOUT_PLUGINS];
26
+
27
+ function schemaPath(...segments: PropertyKey[]): string {
28
+ return JSON.stringify(segments);
29
+ }
30
+
31
+ const ANY_WIDGET_MODEL_ID_PATH = schemaPath("properties", "modelId");
32
+ const DATA_TABLE_STATS_COMPONENTS = new Set([
33
+ "marimo-table.get_column_summaries.output",
34
+ "marimo-table.preview_column.output",
35
+ ]);
36
+ const DATA_TABLE_STATS_FIELDS = [
37
+ "min",
38
+ "max",
39
+ "std",
40
+ "mean",
41
+ "median",
42
+ "p5",
43
+ "p25",
44
+ "p75",
45
+ "p95",
46
+ ] as const;
47
+ const DATA_TABLE_NAN_PATHS = new Set(
48
+ DATA_TABLE_STATS_FIELDS.flatMap((field) => [
49
+ schemaPath("properties", "stats", "properties", field, "anyOf", 1),
50
+ schemaPath(
51
+ "properties",
52
+ "stats",
53
+ "additionalProperties",
54
+ "properties",
55
+ field,
56
+ "anyOf",
57
+ 1,
58
+ ),
59
+ ]),
60
+ );
61
+ const DATA_TABLE_BIN_DATE_PATHS = new Set([
62
+ schemaPath(
63
+ "properties",
64
+ "bin_values",
65
+ "additionalProperties",
66
+ "items",
67
+ "properties",
68
+ "bin_start",
69
+ "anyOf",
70
+ 2,
71
+ ),
72
+ schemaPath(
73
+ "properties",
74
+ "bin_values",
75
+ "additionalProperties",
76
+ "items",
77
+ "properties",
78
+ "bin_end",
79
+ "anyOf",
80
+ 2,
81
+ ),
82
+ ]);
83
+
84
+ const livePluginOverrides: BuildPluginSpecOptions = {
85
+ unrepresentableSchemaOverride: ({ componentName, path, type }) => {
86
+ const pathKey = schemaPath(...path);
87
+
88
+ // These validators accept values that JSON cannot encode directly. Their
89
+ // explicit wire representations live here to avoid adding schema metadata
90
+ // or allocations to production plugin initialization.
91
+ if (
92
+ componentName === "marimo-anywidget.data" &&
93
+ type === "custom" &&
94
+ pathKey === ANY_WIDGET_MODEL_ID_PATH
95
+ ) {
96
+ return { type: "string", minLength: 1 };
97
+ }
98
+ if (
99
+ DATA_TABLE_STATS_COMPONENTS.has(componentName) &&
100
+ type === "nan" &&
101
+ DATA_TABLE_NAN_PATHS.has(pathKey)
102
+ ) {
103
+ return { type: "number" };
104
+ }
105
+ if (
106
+ componentName === "marimo-table.get_column_summaries.output" &&
107
+ type === "custom" &&
108
+ DATA_TABLE_BIN_DATE_PATHS.has(pathKey)
109
+ ) {
110
+ return { type: "string", format: "date-time" };
111
+ }
112
+ return undefined;
113
+ },
114
+ };
115
+
116
+ const document = buildPluginSpec(ALL_PLUGINS, livePluginOverrides);
117
+
118
+ if (process.env.UPDATE_PLUGIN_SCHEMA) {
119
+ writeFileSync(
120
+ SCHEMA_PATH,
121
+ stringify(document, { aliasDuplicateObjects: false }),
122
+ );
123
+ process.stdout.write(`Updated ${SCHEMA_PATH}\n`);
124
+ }
125
+
126
+ function plugin(tagName: string, validator: z.ZodType): PluginContract {
127
+ return { tagName, validator };
128
+ }
129
+
130
+ function contractRefs(
131
+ doc: PluginOpenAPIDocument,
132
+ path: string,
133
+ ): { read: string; write: string } {
134
+ const contract = doc.paths[path] as {
135
+ get: {
136
+ responses: {
137
+ "200": {
138
+ content: { "application/json": { schema: { $ref: string } } };
139
+ };
140
+ };
141
+ };
142
+ put: {
143
+ requestBody: {
144
+ content: { "application/json": { schema: { $ref: string } } };
145
+ };
146
+ };
147
+ };
148
+ return {
149
+ read: contract.get.responses["200"].content["application/json"].schema.$ref,
150
+ write: contract.put.requestBody.content["application/json"].schema.$ref,
151
+ };
152
+ }
153
+
154
+ describe("frontend/plugins.openapi.yaml", () => {
155
+ it(`is in sync with the registered plugin schemas (run \`${REGENERATE_COMMAND}\` to update)`, () => {
156
+ if (!existsSync(SCHEMA_PATH)) {
157
+ throw new Error(
158
+ `Missing generated plugin schema at ${SCHEMA_PATH}. Run \`${REGENERATE_COMMAND}\` and commit the result.`,
159
+ );
160
+ }
161
+ const committed = parse(readFileSync(SCHEMA_PATH, "utf8"));
162
+ expect(
163
+ committed,
164
+ `The committed plugin schema is stale. Run \`${REGENERATE_COMMAND}\` and commit frontend/plugins.openapi.yaml.`,
165
+ ).toEqual(document);
166
+ });
167
+
168
+ it("contains every plugin data and RPC schema", () => {
169
+ const expectedContracts = ALL_PLUGINS.reduce(
170
+ (count, registeredPlugin) =>
171
+ count +
172
+ 1 +
173
+ 2 *
174
+ Object.keys(
175
+ "functions" in registeredPlugin
176
+ ? (registeredPlugin.functions ?? {})
177
+ : {},
178
+ ).length,
179
+ 0,
180
+ );
181
+ expect(
182
+ Object.keys(document.components.schemas).length,
183
+ ).toBeGreaterThanOrEqual(expectedContracts);
184
+ expect(Object.keys(document.paths)).toHaveLength(expectedContracts);
185
+
186
+ for (const registeredPlugin of ALL_PLUGINS as PluginContract[]) {
187
+ const dataName = `${registeredPlugin.tagName}.data`;
188
+ expect(
189
+ Object.hasOwn(document.components.schemas, dataName),
190
+ `Missing data component ${dataName}`,
191
+ ).toBe(true);
192
+ expect(
193
+ Object.hasOwn(
194
+ document.paths,
195
+ `/plugins/${registeredPlugin.tagName}/data`,
196
+ ),
197
+ `Missing data path for ${registeredPlugin.tagName}`,
198
+ ).toBe(true);
199
+
200
+ for (const functionName of Object.keys(
201
+ registeredPlugin.functions ?? {},
202
+ )) {
203
+ for (const direction of ["input", "output"] as const) {
204
+ const componentName = `${registeredPlugin.tagName}.${functionName}.${direction}`;
205
+ expect(
206
+ Object.hasOwn(document.components.schemas, componentName),
207
+ `Missing RPC component ${componentName}`,
208
+ ).toBe(true);
209
+ expect(
210
+ Object.hasOwn(
211
+ document.paths,
212
+ `/plugins/${registeredPlugin.tagName}/functions/${functionName}/${direction}`,
213
+ ),
214
+ `Missing RPC path for ${componentName}`,
215
+ ).toBe(true);
216
+ }
217
+ }
218
+ }
219
+ });
220
+
221
+ it("models representative data and RPC schemas bidirectionally", () => {
222
+ expect(contractRefs(document, "/plugins/marimo-button/data")).toEqual({
223
+ read: "#/components/schemas/marimo-button.data",
224
+ write: "#/components/schemas/marimo-button.data",
225
+ });
226
+ expect(
227
+ contractRefs(document, "/plugins/marimo-table/functions/search/input"),
228
+ ).toEqual({
229
+ read: "#/components/schemas/marimo-table.search.input",
230
+ write: "#/components/schemas/marimo-table.search.input",
231
+ });
232
+ expect(
233
+ contractRefs(document, "/plugins/marimo-table/functions/search/output"),
234
+ ).toEqual({
235
+ read: "#/components/schemas/marimo-table.search.output",
236
+ write: "#/components/schemas/marimo-table.search.output",
237
+ });
238
+ });
239
+
240
+ it("contains no dangling Zod definitions", () => {
241
+ expect(JSON.stringify(document)).not.toContain("#/$defs");
242
+ });
243
+ });
244
+
245
+ describe("buildPluginSpec", () => {
246
+ it("emits accepted input shapes for defaults and transforms", () => {
247
+ const doc = buildPluginSpec([
248
+ plugin(
249
+ "test-input",
250
+ z.object({
251
+ defaulted: z.string().default("default"),
252
+ transformed: z.string().transform((value) => value.length),
253
+ }),
254
+ ),
255
+ ]);
256
+ const schema = doc.components.schemas["test-input.data"] as {
257
+ properties: Record<string, { type: string; default?: unknown }>;
258
+ required?: string[];
259
+ };
260
+
261
+ expect(schema.properties.defaulted).toEqual({
262
+ type: "string",
263
+ default: "default",
264
+ });
265
+ expect(schema.properties.transformed.type).toBe("string");
266
+ expect(schema.required).toEqual(["transformed"]);
267
+ });
268
+
269
+ it("only rewrites shared-definition references", () => {
270
+ const referenceLikeValue = "#/components/schemas/__shared#/$defs/schema0";
271
+ const doc = buildPluginSpec([
272
+ plugin("test-reference-like-value", z.literal(referenceLikeValue)),
273
+ ]);
274
+
275
+ expect(
276
+ doc.components.schemas["test-reference-like-value.data"],
277
+ ).toMatchObject({ const: referenceLikeValue });
278
+ });
279
+
280
+ it("resolves recursive schemas through their OpenAPI component", () => {
281
+ interface RecursiveValue {
282
+ children: RecursiveValue[];
283
+ }
284
+ const recursive: z.ZodType<RecursiveValue> = z.lazy(() =>
285
+ z.object({ children: z.array(recursive) }),
286
+ );
287
+ const doc = buildPluginSpec([plugin("test-recursive", recursive)]);
288
+
289
+ expect(
290
+ JSON.stringify(doc.components.schemas["test-recursive.data"]),
291
+ ).toContain('"$ref":"#/components/schemas/test-recursive.data"');
292
+ expect(JSON.stringify(doc)).not.toContain("#/$defs");
293
+ });
294
+
295
+ it("uses explicit metadata for JSON-unrepresentable schemas", () => {
296
+ const doc = buildPluginSpec([
297
+ plugin(
298
+ "test-date",
299
+ z.instanceof(Date).meta({ type: "string", format: "date-time" }),
300
+ ),
301
+ ]);
302
+ expect(doc.components.schemas["test-date.data"]).toEqual({
303
+ type: "string",
304
+ format: "date-time",
305
+ });
306
+
307
+ expect(() =>
308
+ buildPluginSpec([
309
+ plugin(
310
+ "test-custom",
311
+ z.custom(() => true),
312
+ ),
313
+ ]),
314
+ ).toThrow(/explicit JSON Schema metadata.*test-only override/);
315
+ });
316
+
317
+ it("rejects duplicate tags and operation IDs", () => {
318
+ expect(() =>
319
+ buildPluginSpec([
320
+ plugin("duplicate", z.string()),
321
+ plugin("duplicate", z.number()),
322
+ ]),
323
+ ).toThrow("Duplicate plugin tag name duplicate");
324
+
325
+ expect(() =>
326
+ buildPluginSpec([
327
+ plugin("operation-a-b", z.string()),
328
+ plugin("operation-a_b", z.string()),
329
+ ]),
330
+ ).toThrow("Duplicate OpenAPI operationId");
331
+ });
332
+
333
+ it("rejects unresolved local component references", () => {
334
+ expect(() =>
335
+ buildPluginSpec([
336
+ plugin(
337
+ "test-missing-ref",
338
+ z.string().meta({ $ref: "#/components/schemas/Missing" }),
339
+ ),
340
+ ]),
341
+ ).toThrow("Unresolved OpenAPI component reference");
342
+ });
343
+ });