@marimo-team/islands 0.23.12-dev10 → 0.23.12-dev11
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/{code-visibility-D1W_9sHW.js → code-visibility-Bi2d-Fh5.js} +1 -1
- package/dist/main.js +1084 -988
- package/dist/{reveal-component-BXP8Y-bq.js → reveal-component-OEq7xNow.js} +1 -1
- package/package.json +1 -1
- 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 +293 -13
|
@@ -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);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
ISLAND_DATA_ATTRIBUTES,
|
|
5
5
|
ISLAND_TAG_NAMES,
|
|
6
|
+
ISLANDS_JSON_SCRIPT_TYPE,
|
|
6
7
|
} from "@/core/islands/constants";
|
|
7
8
|
import { Logger } from "@/utils/Logger";
|
|
8
9
|
|
|
@@ -23,6 +24,10 @@ export interface MarimoIslandApp {
|
|
|
23
24
|
* ID since we allow multiple apps on the same page.
|
|
24
25
|
*/
|
|
25
26
|
id: string;
|
|
27
|
+
/**
|
|
28
|
+
* Whether cells came from a supported JSON payload instead of DOM parsing.
|
|
29
|
+
*/
|
|
30
|
+
payloadBacked?: boolean;
|
|
26
31
|
/**
|
|
27
32
|
* Cells in the app.
|
|
28
33
|
*/
|
|
@@ -42,6 +47,30 @@ interface MarimoIslandCell {
|
|
|
42
47
|
* Index of the cell.
|
|
43
48
|
*/
|
|
44
49
|
idx: number;
|
|
50
|
+
/**
|
|
51
|
+
* Stable cell identifier, when provided by the island payload.
|
|
52
|
+
*/
|
|
53
|
+
cellId?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the generated marimo cell should be present but not executed.
|
|
56
|
+
*/
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface MarimoIslandPayload {
|
|
61
|
+
schemaVersion: 1;
|
|
62
|
+
appId: string;
|
|
63
|
+
cells: MarimoIslandPayloadCell[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface MarimoIslandPayloadCell {
|
|
67
|
+
cellId: string;
|
|
68
|
+
code: string;
|
|
69
|
+
outputHtml: string;
|
|
70
|
+
outputMimetype: string;
|
|
71
|
+
reactive: boolean;
|
|
72
|
+
displayCode: boolean;
|
|
73
|
+
displayOutput: boolean;
|
|
45
74
|
}
|
|
46
75
|
|
|
47
76
|
/**
|
|
@@ -51,14 +80,20 @@ interface MarimoIslandCell {
|
|
|
51
80
|
export function parseMarimoIslandApps(
|
|
52
81
|
root: Document | Element = document,
|
|
53
82
|
): MarimoIslandApp[] {
|
|
54
|
-
const embeds =
|
|
83
|
+
const embeds = [
|
|
84
|
+
...root.querySelectorAll<HTMLElement>(ISLAND_TAG_NAMES.ISLAND),
|
|
85
|
+
];
|
|
86
|
+
const payloads = parseMarimoIslandPayloads(root);
|
|
55
87
|
if (embeds.length === 0) {
|
|
56
88
|
Logger.warn("No embedded marimo apps found.");
|
|
57
89
|
return [];
|
|
58
90
|
}
|
|
59
91
|
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
if (payloads.length > 0) {
|
|
93
|
+
return parsePayloadBackedApps(embeds, payloads);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return parseIslandElementsIntoApps(embeds);
|
|
62
97
|
}
|
|
63
98
|
|
|
64
99
|
/**
|
|
@@ -90,11 +125,12 @@ export function parseIslandElementsIntoApps(
|
|
|
90
125
|
continue;
|
|
91
126
|
}
|
|
92
127
|
|
|
93
|
-
|
|
94
|
-
|
|
128
|
+
let app = apps.get(appId);
|
|
129
|
+
if (!app) {
|
|
130
|
+
app = { id: appId, cells: [] };
|
|
131
|
+
apps.set(appId, app);
|
|
95
132
|
}
|
|
96
133
|
|
|
97
|
-
const app = apps.get(appId)!;
|
|
98
134
|
const idx = app.cells.length;
|
|
99
135
|
app.cells.push({
|
|
100
136
|
output: cellData.output,
|
|
@@ -109,6 +145,159 @@ export function parseIslandElementsIntoApps(
|
|
|
109
145
|
return [...apps.values()];
|
|
110
146
|
}
|
|
111
147
|
|
|
148
|
+
function parsePayloadBackedApps(
|
|
149
|
+
embeds: HTMLElement[],
|
|
150
|
+
payloads: MarimoIslandPayload[],
|
|
151
|
+
): MarimoIslandApp[] {
|
|
152
|
+
const apps = new Map<string, MarimoIslandApp>();
|
|
153
|
+
const matchedPayloadCells = new Map<MarimoIslandPayloadCell, HTMLElement>();
|
|
154
|
+
const consumedEmbeds = new Set<HTMLElement>();
|
|
155
|
+
const acceptedPayloads: MarimoIslandPayload[] = [];
|
|
156
|
+
|
|
157
|
+
for (const payload of payloads) {
|
|
158
|
+
let hasMatchedIsland = false;
|
|
159
|
+
for (const cell of payload.cells) {
|
|
160
|
+
const embed = findMatchingIsland({
|
|
161
|
+
embeds,
|
|
162
|
+
appId: payload.appId,
|
|
163
|
+
cell,
|
|
164
|
+
consumedEmbeds,
|
|
165
|
+
});
|
|
166
|
+
if (!embed) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
consumedEmbeds.add(embed);
|
|
170
|
+
matchedPayloadCells.set(cell, embed);
|
|
171
|
+
materializeIslandPayload(embed, cell);
|
|
172
|
+
hasMatchedIsland = true;
|
|
173
|
+
}
|
|
174
|
+
// Only payloads matched to island anchors can start runtime apps.
|
|
175
|
+
if (hasMatchedIsland) {
|
|
176
|
+
acceptedPayloads.push(payload);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const payloadAppIds = new Set(
|
|
181
|
+
acceptedPayloads.map((payload) => payload.appId),
|
|
182
|
+
);
|
|
183
|
+
const reactivePayloadAppIds = new Set(
|
|
184
|
+
acceptedPayloads
|
|
185
|
+
.filter((payload) => payload.cells.some((cell) => cell.reactive))
|
|
186
|
+
.map((payload) => payload.appId),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
for (const payload of acceptedPayloads) {
|
|
190
|
+
for (const cell of payload.cells) {
|
|
191
|
+
const embed = matchedPayloadCells.get(cell);
|
|
192
|
+
// Static-only payload apps render from HTML and do not need a Pyodide
|
|
193
|
+
// session.
|
|
194
|
+
if (!reactivePayloadAppIds.has(payload.appId)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let app = apps.get(payload.appId);
|
|
199
|
+
if (!app) {
|
|
200
|
+
app = { id: payload.appId, payloadBacked: true, cells: [] };
|
|
201
|
+
apps.set(payload.appId, app);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const idx = app.cells.length;
|
|
205
|
+
const appCell: MarimoIslandCell = {
|
|
206
|
+
cellId: cell.cellId,
|
|
207
|
+
output: cell.outputHtml,
|
|
208
|
+
code: cell.reactive ? cell.code : "",
|
|
209
|
+
idx: idx,
|
|
210
|
+
};
|
|
211
|
+
// Keep static cells in the generated file so later reactive cells keep
|
|
212
|
+
// stable runtime indices without executing static code.
|
|
213
|
+
if (!cell.reactive) {
|
|
214
|
+
appCell.disabled = true;
|
|
215
|
+
}
|
|
216
|
+
app.cells.push(appCell);
|
|
217
|
+
if (cell.reactive) {
|
|
218
|
+
embed?.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, idx.toString());
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// A supported payload is the runtime source for its app. Extra same-app DOM
|
|
224
|
+
// islands are disconnected from runtime binding.
|
|
225
|
+
for (const embed of embeds) {
|
|
226
|
+
const appId = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID);
|
|
227
|
+
if (appId && payloadAppIds.has(appId) && !consumedEmbeds.has(embed)) {
|
|
228
|
+
embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID);
|
|
229
|
+
embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX);
|
|
230
|
+
embed.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "false");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const domOnlyEmbeds = embeds.filter((embed) => {
|
|
235
|
+
const appId = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID);
|
|
236
|
+
return !appId || !payloadAppIds.has(appId);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return [...apps.values(), ...parseIslandElementsIntoApps(domOnlyEmbeds)];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function findMatchingIsland({
|
|
243
|
+
embeds,
|
|
244
|
+
appId,
|
|
245
|
+
cell,
|
|
246
|
+
consumedEmbeds,
|
|
247
|
+
}: {
|
|
248
|
+
embeds: HTMLElement[];
|
|
249
|
+
appId: string;
|
|
250
|
+
cell: MarimoIslandPayloadCell;
|
|
251
|
+
consumedEmbeds: Set<HTMLElement>;
|
|
252
|
+
}): HTMLElement | undefined {
|
|
253
|
+
return embeds.find((embed) => {
|
|
254
|
+
if (consumedEmbeds.has(embed)) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
return (
|
|
258
|
+
embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID) === appId &&
|
|
259
|
+
embed.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID) === cell.cellId
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function materializeIslandPayload(
|
|
265
|
+
embed: HTMLElement,
|
|
266
|
+
cell: MarimoIslandPayloadCell,
|
|
267
|
+
): void {
|
|
268
|
+
embed.setAttribute(
|
|
269
|
+
ISLAND_DATA_ATTRIBUTES.REACTIVE,
|
|
270
|
+
JSON.stringify(cell.reactive),
|
|
271
|
+
);
|
|
272
|
+
// The runtime file is synthesized from payload order, so DOM anchors bind
|
|
273
|
+
// by index.
|
|
274
|
+
embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID);
|
|
275
|
+
if (!cell.reactive) {
|
|
276
|
+
embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const output = ensureIslandChild(embed, ISLAND_TAG_NAMES.CELL_OUTPUT);
|
|
280
|
+
output.innerHTML = cell.displayOutput ? cell.outputHtml : "";
|
|
281
|
+
|
|
282
|
+
const code = ensureIslandChild(embed, ISLAND_TAG_NAMES.CELL_CODE);
|
|
283
|
+
code.hidden = true;
|
|
284
|
+
code.textContent = encodeURIComponent(cell.code);
|
|
285
|
+
|
|
286
|
+
const editor = embed.querySelector<HTMLElement>(ISLAND_TAG_NAMES.CODE_EDITOR);
|
|
287
|
+
if (editor) {
|
|
288
|
+
editor.setAttribute("data-initial-value", JSON.stringify(cell.code));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function ensureIslandChild(embed: HTMLElement, tagName: string): HTMLElement {
|
|
293
|
+
let child = embed.querySelector<HTMLElement>(tagName);
|
|
294
|
+
if (!child) {
|
|
295
|
+
child = embed.ownerDocument.createElement(tagName);
|
|
296
|
+
embed.appendChild(child);
|
|
297
|
+
}
|
|
298
|
+
return child;
|
|
299
|
+
}
|
|
300
|
+
|
|
112
301
|
/**
|
|
113
302
|
* Parses a single island element into cell data
|
|
114
303
|
* @param embed - The island HTML element
|
|
@@ -132,26 +321,35 @@ export function parseIslandElement(
|
|
|
132
321
|
};
|
|
133
322
|
}
|
|
134
323
|
|
|
135
|
-
export function createMarimoFile(app: {
|
|
324
|
+
export function createMarimoFile(app: {
|
|
325
|
+
cells: { code: string; disabled?: boolean }[];
|
|
326
|
+
}): string {
|
|
136
327
|
const lines = [
|
|
137
328
|
"import marimo",
|
|
138
329
|
"app = marimo.App()",
|
|
139
330
|
app.cells
|
|
140
331
|
.map((cell) => {
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
332
|
+
// Disabled payload cells are placeholders. Emit pass so static code
|
|
333
|
+
// does not define names in the runtime graph.
|
|
334
|
+
const sourceCode = cell.disabled ? "" : cell.code;
|
|
335
|
+
const code = sourceCode
|
|
336
|
+
? sourceCode
|
|
337
|
+
.split("\n")
|
|
338
|
+
.map((line) => ` ${line}`)
|
|
339
|
+
.join("\n")
|
|
340
|
+
: " pass";
|
|
146
341
|
|
|
147
342
|
// TODO: Handle async cells better
|
|
148
343
|
// This is probably not the best way to check if the code is async
|
|
149
344
|
// Ideally this is pushed into the Python code
|
|
150
345
|
const isAsync = code.includes("await ");
|
|
151
346
|
const prefix = isAsync ? "async def" : "def";
|
|
347
|
+
const decorator = cell.disabled
|
|
348
|
+
? "@app.cell(disabled=True)"
|
|
349
|
+
: "@app.cell";
|
|
152
350
|
|
|
153
351
|
// Wrap in a function
|
|
154
|
-
return
|
|
352
|
+
return `${decorator}\n${prefix} __():\n${code}\n return`;
|
|
155
353
|
})
|
|
156
354
|
.join("\n"),
|
|
157
355
|
];
|
|
@@ -204,3 +402,85 @@ export function extractIslandCodeFromEmbed(embed: HTMLElement): string {
|
|
|
204
402
|
|
|
205
403
|
return "";
|
|
206
404
|
}
|
|
405
|
+
|
|
406
|
+
function parseMarimoIslandPayloads(
|
|
407
|
+
root: Document | Element,
|
|
408
|
+
): MarimoIslandPayload[] {
|
|
409
|
+
const scripts = root.querySelectorAll<HTMLScriptElement>(
|
|
410
|
+
`script[type="${ISLANDS_JSON_SCRIPT_TYPE}"]`,
|
|
411
|
+
);
|
|
412
|
+
const payloads: MarimoIslandPayload[] = [];
|
|
413
|
+
|
|
414
|
+
for (const script of scripts) {
|
|
415
|
+
if (isNestedIslandPayloadScript(script)) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const payload = parseMarimoIslandPayload(script.textContent);
|
|
419
|
+
if (payload) {
|
|
420
|
+
payloads.push(payload);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return payloads;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isNestedIslandPayloadScript(script: HTMLScriptElement): boolean {
|
|
428
|
+
return Boolean(
|
|
429
|
+
script.closest(ISLAND_TAG_NAMES.ISLAND) ||
|
|
430
|
+
script.closest(ISLAND_TAG_NAMES.CELL_OUTPUT),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function parseMarimoIslandPayload(
|
|
435
|
+
text: string | undefined | null,
|
|
436
|
+
): MarimoIslandPayload | null {
|
|
437
|
+
if (!text) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const payload = JSON.parse(text);
|
|
443
|
+
if (isMarimoIslandPayload(payload)) {
|
|
444
|
+
return payload;
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isMarimoIslandPayload(
|
|
454
|
+
payload: unknown,
|
|
455
|
+
): payload is MarimoIslandPayload {
|
|
456
|
+
if (!isRecord(payload)) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
return (
|
|
460
|
+
payload.schemaVersion === 1 &&
|
|
461
|
+
typeof payload.appId === "string" &&
|
|
462
|
+
Array.isArray(payload.cells) &&
|
|
463
|
+
payload.cells.every(isMarimoIslandPayloadCell)
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isMarimoIslandPayloadCell(
|
|
468
|
+
cell: unknown,
|
|
469
|
+
): cell is MarimoIslandPayloadCell {
|
|
470
|
+
if (!isRecord(cell)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
return (
|
|
474
|
+
typeof cell.cellId === "string" &&
|
|
475
|
+
typeof cell.code === "string" &&
|
|
476
|
+
typeof cell.outputHtml === "string" &&
|
|
477
|
+
typeof cell.outputMimetype === "string" &&
|
|
478
|
+
typeof cell.reactive === "boolean" &&
|
|
479
|
+
typeof cell.displayCode === "boolean" &&
|
|
480
|
+
typeof cell.displayOutput === "boolean"
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
485
|
+
return typeof value === "object" && value !== null;
|
|
486
|
+
}
|