grist-widget-sdk 0.4.0 → 0.5.1

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 (37) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +79 -208
  3. package/dist/app.cjs +186 -65
  4. package/dist/app.cjs.map +1 -1
  5. package/dist/app.d.cts +5 -100
  6. package/dist/app.d.ts +5 -100
  7. package/dist/app.js +4 -2
  8. package/dist/{chunk-O7NY46QY.js → chunk-MAPAWVQC.js} +88 -20
  9. package/dist/chunk-MAPAWVQC.js.map +1 -0
  10. package/dist/{chunk-B5ZZNLVS.js → chunk-OXA2VITC.js} +5 -1
  11. package/dist/chunk-OXA2VITC.js.map +1 -0
  12. package/dist/{chunk-P6VTEKWO.js → chunk-P5I5MRC6.js} +86 -10
  13. package/dist/chunk-P5I5MRC6.js.map +1 -0
  14. package/dist/{use-grist-BK0eke9K.d.cts → define-widget-B1FIvI-h.d.cts} +179 -104
  15. package/dist/{use-grist-C5DMZ3wU.d.ts → define-widget-B9gEtJP0.d.ts} +179 -104
  16. package/dist/emulator/testing.cjs +2393 -2243
  17. package/dist/emulator/testing.cjs.map +1 -1
  18. package/dist/emulator/testing.d.cts +27 -9
  19. package/dist/emulator/testing.d.ts +27 -9
  20. package/dist/emulator/testing.js +75 -55
  21. package/dist/emulator/testing.js.map +1 -1
  22. package/dist/emulator.cjs +4 -0
  23. package/dist/emulator.cjs.map +1 -1
  24. package/dist/emulator.d.cts +3 -3
  25. package/dist/emulator.d.ts +3 -3
  26. package/dist/emulator.js +1 -1
  27. package/dist/index.cjs +133 -67
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +2 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +5 -1
  32. package/dist/mount-grist-widget-CkB-3iG_.d.ts +137 -0
  33. package/dist/mount-grist-widget-Dtelqao_.d.cts +137 -0
  34. package/package.json +1 -1
  35. package/dist/chunk-B5ZZNLVS.js.map +0 -1
  36. package/dist/chunk-O7NY46QY.js.map +0 -1
  37. package/dist/chunk-P6VTEKWO.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # grist-widget-sdk
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 10015f7: **`mountGristWidget(entry)` and `smokeTestWidget(entry)` read an entry module that also exports its component as `App`.** A widget whose `src/app/App.tsx` has `export function App()` next to `export default App` was taken for the older `{ App, options }` form: `mountGristWidget(entry)` (the 0.5 template's `src/main.tsx`) mounted it without its `GRIST_OPTIONS` (no declared columns, no access level), and `smokeTestWidget(entry)` threw. An entry module is now recognised by its default export.
8
+
9
+ Existing widgets: nothing to do. A widget on `create-grist-widget` 0.5.0's template with a named `App` export gets its declaration back once this version is installed.
10
+
11
+ ## 0.5.0
12
+
13
+ ### Minor Changes
14
+
15
+ - d3d9232: **`defineGristWidget()`: one declaration types, mounts and tests a widget.**
16
+
17
+ ```ts
18
+ export const widget = defineGristWidget({
19
+ title: "Tasks",
20
+ requiredAccess: "full",
21
+ columns: [{ name: "Title", type: "Text" }, { name: "Done", type: "Bool" }],
22
+ })
23
+ const w = widget.useGrist() // mappedRecord typed from the declaration: no generics, no `as const`
24
+ await w.updateRecord({ Done: true }) // the selected row, encoded for each declared type
25
+ ```
26
+
27
+ - `defineGristWidget(definition)` keeps the column names and types literal (a `const` type parameter), so `widget.useGrist()` types `mappedRecord`, `mappedRecords`, `mapBack` and the writes from the declaration. `GristWidgetRow<typeof widget>` names the record type.
28
+ - `useGrist()` gains `updateRecord(patch)` / `updateRecord(rowId, patch)` and `addRecord(patch)`: `mapBack` + encoding + `table.update` / `table.create` in one call, under the declared names.
29
+ - `mountGristWidget(entry, { dev })` takes the widget's entry module (`import * as entry from "./app/App"`) and reads its exports: `default`, `widget` (or `GRIST_OPTIONS` and `WIDGET_METADATA`), and optionally `Alerts`, `LocalDevPage`, `ReleasePage`. A widget's customisations then live in its own file instead of `src/main.tsx`. The declaration's title and description set the page's title and meta tags. `mountGristWidget({ App, options, dev })` still works.
30
+ - `renderWithGrist(<App />, { widget, rows })` runs the widget as `mountGristWidget()` does inside Grist, against a table with a column per declared column, every one mapped, seeded with typed rows: no document or mapping setup in a test. `smokeTestWidget(entry)` takes the entry module as `mountGristWidget()` does (or `{ App, widget }`), so one smoke test fits a widget declared either way.
31
+
32
+ Nothing changes for an existing widget: `GRIST_OPTIONS`, `useGrist<TRow, TMapped>()`, `mapBack` and `table.update` work as before.
33
+
34
+ ### Patch Changes
35
+
36
+ - 734d307: **`pnpm dev` opens the widget in real Grist, in the browser, with no Docker.** The template ships its demo page (`public/demo/`, a document for its widget and the page that opens it in grist-static), and on the dev server `http://localhost:5173/demo/index.html` runs that Grist on the dev server's own widget: edits reload in place, writes land in the document. The SDK's local-dev page links to it (or says how to get it, `pnpm demo --save`, when the widget has no demo page yet). The scaffold puts the widget's title in the page.
37
+
38
+ Existing widgets can do nothing: those with a demo page (`pnpm demo --save`) already get it on `pnpm dev` at `/demo/index.html`; the others get it once they run `pnpm demo --save` (Docker, once).
39
+ - dfb2e66: **Writes are the mirror of typed reads.** `mapBack` now encodes each value for the type its column declares: a ChoiceList's `string[]`, a RefList's row ids or an Attachments list become `["L", ...]`, a `Date` becomes seconds, a `Reference` its row id. Before, writing back what `mappedRecord` gave stored a ChoiceList or RefList cell as an error (`["E", "KeyError"]`, checked in Grist 1.7.22), because Grist's plugin API writes values as given. `encodeGristCell(type, value)` is exported for writes that don't go through `mapBack`; a value already encoded passes through.
40
+
41
+ The emulator now stores a bare list written to a list column as Grist does, an error cell reported in `cellIssues`, so `pnpm test` shows the mistake instead of hiding it.
42
+
43
+ Overlapping writes count as one run: `actionStatus` stays `"running"` until the last one ends, then is `"error"` if any failed (the first write to end used to set it back to `"idle"`). A failed write nobody awaits no longer logs an unhandled rejection; the alert reports it, and the promise still rejects for whoever awaits it.
44
+
45
+ What to change, if a widget relies on it: a widget that already encoded lists itself (`["L", ...]`, `encodeGristValue`, the attachment helpers) needs nothing. A test that wrote a bare array to a RefList through the emulator now gets an error cell: encode it, or go through `mapBack`.
46
+ - ca3f010: `GristMappedRow` now types every declared column as its value or `null`, `Text` and `Bool` included (`Title: string | null`, not `string`). It promised a non-null value for those types while `mappedRecord` holds `null` for a cell that doesn't match its declared type, so a widget trusting the type could crash on a formula error. A widget that reads a `Text` or `Bool` column from a `GristMappedRow` may need a fallback (`row.Title ?? ""`) to compile.
47
+
48
+ The package README, shown on npm, describes the 0.4.0 surface; it still listed `grist-widget-sdk/advanced`, the slice hooks and the emulator's iframe transport and devtools, all removed in 0.4.0.
49
+
3
50
  ## 0.4.0
4
51
 
5
52
  ### Minor Changes
package/README.md CHANGED
@@ -1,249 +1,120 @@
1
1
  # grist-widget-sdk
2
2
 
3
- React hooks and TypeScript utilities for building Grist widgets.
3
+ React hooks for [Grist](https://www.getgrist.com) custom widgets: one
4
+ `useGrist()` for the connection, the selected record and the section's
5
+ records, column mapping, typed reads, writes, widget options and the
6
+ document's REST API, plus an emulator to test a widget without Grist.
4
7
 
5
- ## Why not use `grist` directly?
8
+ Documentation: <https://gristwidgets.com/docs>. To start a widget, use the
9
+ scaffold, which wires all of this up:
6
10
 
7
- Grist’s [`grist-plugin-api.js`](https://docs.getgrist.com/grist-plugin-api.js) exposes a global `grist` object. For React widgets, **`grist-widget-sdk`** replaces hand-rolled glue:
11
+ ```bash
12
+ npm create grist-widget my-widget
13
+ ```
14
+
15
+ ## Why not `grist` directly?
8
16
 
9
17
  | Raw `grist` | SDK |
10
18
  | --- | --- |
11
- | `grist.ready` + `onRecord` callbacks | `<GristWidgetProvider>` + `useGrist()` with `mode` |
12
- | Wire-format dates/refs | Decoded values by default |
13
- | `mapColumnNames` / tuples for writes | `mappedRecord`, `mapBack`, `table.update` |
14
- | No test harness | `renderWithGrist` + emulator presets |
19
+ | `grist.ready` and `onRecord` callbacks | `useGrist()`: `status`, `record`, `records`, `mode` |
20
+ | Wire-format dates, refs and errors | Each mapped cell as its declared type (`mappedRecord`), mismatches in `cellIssues` |
21
+ | `mapColumnNames`, tuples for writes | `mappedRecord`, `mapBack`, `table.update` |
22
+ | No test harness | `renderWithGrist` with emulator presets |
15
23
 
16
24
  Full comparison: [Raw plugin API vs SDK](https://gristwidgets.com/docs/sdk/guide/raw-plugin-api-vs-sdk).
17
25
 
18
- ## What This Package Provides
19
-
20
- - Main API via `grist-widget-sdk` (hooks, types, helpers)
21
- - `grist-widget-sdk/advanced` — lower-level hooks (`useGristReady`, `useGristHandshake`, …)
22
- - `grist-widget-sdk/emulator` subpath for emulator utilities
23
- - ESM + CJS + type declaration builds (`.d.ts`)
24
-
25
- ## Installation
26
+ ## Install
26
27
 
27
28
  ```bash
28
29
  pnpm add grist-widget-sdk
29
30
  ```
30
31
 
31
- Required peer dependencies:
32
-
33
- - `react >= 18`
34
- - `react-dom >= 18`
35
-
36
- ## Grist Runtime Script (Required)
37
-
38
- This package expects a global `grist` object at runtime.
39
-
40
- Load the official Grist plugin API script in your app shell/layout:
32
+ Peer dependencies: `react` and `react-dom` 18 or later. The page must load
33
+ Grist's plugin API, which defines the `grist` global:
41
34
 
42
35
  ```html
43
36
  <script src="https://docs.getgrist.com/grist-plugin-api.js"></script>
44
37
  ```
45
38
 
46
- ## Quick Start Next.js (`app/layout.tsx`)
39
+ ## A widget
47
40
 
48
41
  ```tsx
49
- import Script from "next/script"
50
- import { GristBoundary, GristWidgetProvider } from "grist-widget-sdk"
51
-
52
- export default function RootLayout({ children }: { children: React.ReactNode }) {
53
- return (
54
- <html lang="en">
55
- <body>
56
- <Script src="https://docs.getgrist.com/grist-plugin-api.js" />
57
- <GristWidgetProvider options={{ requiredAccess: "read table" }}>
58
- <GristBoundary>
59
- {children}
60
- </GristBoundary>
61
- </GristWidgetProvider>
62
- </body>
63
- </html>
64
- )
42
+ // src/app/App.tsx
43
+ import { defineGristWidget } from "grist-widget-sdk"
44
+
45
+ export const widget = defineGristWidget({
46
+ title: "Tasks",
47
+ requiredAccess: "full",
48
+ columns: [{ name: "Title", type: "Text" }, { name: "Done", type: "Bool" }],
49
+ })
50
+
51
+ export default function App() {
52
+ const w = widget.useGrist() // w.mappedRecord: { id; Title: string | null; Done: boolean | null } | null
53
+ if (w.mode !== "row" || !w.mappedRecord) return <p>Select a task.</p>
54
+ return <button onClick={() => w.updateRecord({ Done: true })}>{w.mappedRecord.Title}</button>
65
55
  }
66
56
  ```
67
57
 
68
- Then render widget content from your page, for example `app/page.tsx`:
69
-
70
58
  ```tsx
71
- import { useGrist } from "grist-widget-sdk"
72
-
73
- export default function Page() {
74
- const w = useGrist()
75
-
76
- if (w.status === "booting") return <p>Loading...</p>
77
- if (w.mode === "empty") return <p>Select a row.</p>
78
- if (w.mode === "new-row") return <p>New row flow</p>
59
+ // src/main.tsx
60
+ import { mountGristWidget } from "grist-widget-sdk/app"
61
+ import * as entry from "./app/App"
79
62
 
80
- return <p>Record id: {String(w.record?.id ?? "")}</p>
81
- }
63
+ mountGristWidget(entry, { dev: import.meta.env.DEV })
82
64
  ```
83
65
 
84
- ## Quick Start Vite (`src/main.tsx`)
85
-
86
- In `index.html`, load the script:
87
-
88
- ```html
89
- <script src="https://docs.getgrist.com/grist-plugin-api.js"></script>
90
- ```
91
-
92
- Then mount your widget root:
66
+ The declaration types everything with no generics: the mapped records, and
67
+ the writes (`updateRecord`, `addRecord`), which encode each value for its
68
+ declared type. `mountGristWidget()` reads `App.tsx`'s exports: the widget,
69
+ its declaration (for the access, the columns and the page title), and any
70
+ page it replaces. Inside Grist it renders the widget in `GristWidgetProvider`
71
+ and `GristBoundary` (the connecting, access and column-mapping screens), with
72
+ the SDK's alerts, in Grist's light or dark theme.
73
+
74
+ ## Entry points
75
+
76
+ - **`grist-widget-sdk`**: `defineGristWidget`, `useGrist`, `useGristContext`, `useGristOptional`,
77
+ `GristWidgetProvider`, `GristBoundary`, `useGristSchema`,
78
+ `useWidgetMetadata`, `checkGristCell`, `encodeGristCell`, the value codec, the attachment
79
+ helpers, the user-action builders for `applyActions`, and Grist's types.
80
+ [API reference](https://gristwidgets.com/docs/sdk/api).
81
+ - **`grist-widget-sdk/app`**: `mountGristWidget`, `GristWidgetApp`,
82
+ `useTheme`, and the deploy-channel helpers the scaffold's pages use.
83
+ [Reference](https://gristwidgets.com/docs/sdk/api/app).
84
+ - **`grist-widget-sdk/emulator`**: `createGristEmulator`, document
85
+ `presets`, `parseDocument`. [Reference](https://gristwidgets.com/docs/emulator/api).
86
+ - **`grist-widget-sdk/emulator/testing`**: `renderWithGrist`,
87
+ `smokeTestWidget`, `installJsdomStubs`, the matchers (`actionsOf`,
88
+ `eventsOf`, `waitForEvent`), `presets`, and the usual
89
+ `@testing-library/react` helpers. [Testing](https://gristwidgets.com/docs/emulator/testing).
90
+ - **`grist-widget-sdk/tsconfig/app.json`**, **`/tsconfig/node.json`**: the
91
+ compiler settings a scaffolded widget extends.
92
+
93
+ ESM and CJS builds, with type declarations.
94
+
95
+ ## Testing a widget
93
96
 
94
97
  ```tsx
95
- import { StrictMode } from "react"
96
- import { createRoot } from "react-dom/client"
97
- import { GristBoundary, GristWidgetProvider, useGrist } from "grist-widget-sdk"
98
-
99
- function WidgetContent() {
100
- const w = useGrist()
101
-
102
- if (w.status === "booting") return <p>Loading...</p>
103
- if (w.mode === "empty") return <p>Select a row.</p>
104
- if (w.mode === "new-row") return <p>New row flow</p>
105
-
106
- return <p>Record id: {String(w.record?.id ?? "")}</p>
107
- }
108
-
109
- function App() {
110
- return (
111
- <GristWidgetProvider options={{ requiredAccess: "read table" }}>
112
- <GristBoundary>
113
- <WidgetContent />
114
- </GristBoundary>
115
- </GristWidgetProvider>
116
- )
117
- }
118
-
119
- createRoot(document.getElementById("root")!).render(
120
- <StrictMode>
121
- <App />
122
- </StrictMode>
123
- )
98
+ import { fireEvent, renderWithGrist, screen, waitFor } from "grist-widget-sdk/emulator/testing"
99
+ import App, { widget } from "./App"
100
+
101
+ it("marks the task done", async () => {
102
+ const { emulator } = renderWithGrist(<App />, { widget, rows: [{ Title: "Ship", Done: false }] })
103
+ fireEvent.click(await screen.findByRole("button", { name: "Ship" }))
104
+ await waitFor(() => expect(emulator.state.tables.get("Table1")!.rows[0]).toMatchObject({ Done: true }))
105
+ })
124
106
  ```
125
107
 
126
- ## Exports
127
-
128
- Main entrypoint:
129
-
130
- - `grist-widget-sdk` — production surface:
131
- - Components: `GristWidgetProvider`, `GristBoundary`, `useGristContext`, `useGristOptional`
132
- - Primary hook: `useGrist` (includes `capabilities` — `canRender`, `canWriteRecords`, …)
133
- - Slice hooks: `useGristStatus`, `useGristSelection`, `useGristWrites`, `useGristTheme`
134
- - Schema hook: `useGristSchema`
135
- - Helpers: action builders, value codec, safe parsing, replica builders,
136
- REST auth, attachments, column-mapping validation, alerts.
137
-
138
- Subpath exports:
139
-
140
- - `grist-widget-sdk/advanced` — low-level / mid-level hooks for power users
141
- (`useGristReady`, `useGristAvailability`, `useGristRecord`, `useGristRecords`,
142
- `useGristActions`, `useGristTableOps`, `useGristMappedRecord`,
143
- `useGristRowsFromTable`, `useGristAccessToken`, `useGristSection`,
144
- `useGristWidgetOptions`, `useGristTable`).
145
- - `grist-widget-sdk/emulator` — `createGristEmulator`, `parseDocument`,
146
- `safeParseDocument`, `presets`, emulator types.
147
-
148
- ## Replica document (`GristReplicaDocument`)
108
+ The emulator runs in the test process and stands in for `window.grist`. It
109
+ is a test harness, not a preview: to see a widget run, use a real Grist
110
+ (`pnpm demo` in a scaffolded widget, or its demo page).
149
111
 
150
- One JSON shape is used for **`useGristSchema()`**, **`getDocumentJson` / `buildDocumentJson`**, **`*.grist.json`** fixtures, and **`createGristEmulator({ document })`**. Valid fixtures are checked with **`parseDocument`** (Zod).
112
+ ## Developing this package
151
113
 
152
- ### Top-level fields
153
-
154
- | Field | Required | Notes |
155
- | --- | --- | --- |
156
- | `generatedAt` | yes | ISO 8601 string |
157
- | `tables` | yes | `Record<tableId, GristReplicaTable>` — keys align with `docApi.listTables()` / `fetchTable(tableId)` |
158
- | `source` | no | Provenance (e.g. pass `useGristSchema({ source })` or `buildReplicaDocumentFromDocApi(docApi, { source })`); hosts may log it |
159
- | `docName`, `mode`, `selection`, `widgetOptions`, `widgetInteraction`, `configuration`, `accessToken` | no | Same semantics as in types |
160
-
161
- Optional **`mode`**: `"schema-only"` \| `"schema+samples"` \| `"schema+data"` (hint only). At boundaries, use **`normalizeReplicaTableInput`** / **`normalizeReplicaDocumentInput`** so missing **`rows`** behaves like **`[]`**.
162
-
163
- ### Tables and rows
164
-
165
- - **`GristReplicaTable`**: optional `label`, `columns`, `rows`, `rowCount`, `dataOmitted`, etc.
166
- - Row bodies are **`GristReplicaRow`** (`{ id: number, … }`), **not** columnar `fetchTable` wire format. Use **`buildReplicaDocumentFromDocApi`** or **`decodeColumnarToReplicaRows`** to build rows from `docApi.fetchTable`.
167
-
168
- ### Building and serializing
169
-
170
- - Prefer **`buildReplicaDocumentFromDocApi(docApi, { includeRows: false \| "samples" \| "full", sampleRowLimit, source, includeSystemTableData })`**, or **`w.buildReplicaDocumentFromDocApi(...)`** from **`useGrist()`**.
171
- - **`getDocumentJson` / `buildDocumentJson`** accept **`includeRows`**: `"none"` \| `"samples"` \| `"full"` (plus optional **`sampleRowLimit`**, **`space`**).
172
-
173
- ### Emulator
174
-
175
- Only a full **`GristReplicaDocument`** is accepted: **`createGristEmulator({ document })`** and **`parseDocument`**. There is no legacy coercion or alternate “emulator input” type; assemble the object (or load JSON), then **`parseDocument`** if you need validation.
176
-
177
- The canonical design document for this shape is [Replica document](https://gristwidgets.com/docs/sdk/design/replica-document).
178
- - `grist-widget-sdk/emulator/transports` — low-level `iframe` and `inline` transports if you need to wire your own host.
179
- - `grist-widget-sdk/emulator/devtools` — `<EmulatorInspector />` React component (table editor + event timeline + options).
180
- - `grist-widget-sdk/emulator/testing` — `renderWithGrist(...)`, vitest
181
- matchers (`actionsOf`, `eventsOf`, `waitForEvent`), document `presets`,
182
- and the most-used `@testing-library/react` primitives (`screen`,
183
- `fireEvent`, `waitFor`, `act`, `cleanup`, `within`, `render`) so a typical
184
- test file only needs one import line.
185
-
186
- ## Emulator (SOTA)
187
-
188
- The emulator is the single source of truth for three use-cases:
189
-
190
- 1. **Demoing widgets** — production-fidelity by loading the *real*
191
- `grist-plugin-api.js` inside an iframe and speaking `grain-rpc` over
192
- `postMessage`:
193
-
194
- ```tsx
195
- import { createGristEmulator, presets } from "grist-widget-sdk/emulator"
196
- import { EmulatorInspector } from "grist-widget-sdk/emulator/devtools"
197
-
198
- const iframe = document.querySelector("iframe")!
199
- const emulator = createGristEmulator({
200
- document: presets.todoList(),
201
- transport: { kind: "iframe", iframe },
202
- })
203
- ```
204
-
205
- 2. **Robust SDK testing** — `renderWithGrist` synthesises `window.grist`
206
- in-process, so every `useGrist` test goes through the *exact* same code
207
- path without ceremony:
208
-
209
- ```ts
210
- import { renderWithGrist } from "grist-widget-sdk/emulator/testing"
211
-
212
- const { emulator, getByText } = renderWithGrist(<MyWidget />, {
213
- emulator: { document: presets.contacts() },
214
- })
215
- emulator.mutate.addRow("Contacts", { firstName: "Grace", lastName: "Hopper" })
216
- ```
217
-
218
- 3. **Live IDE preview** — same `inline` transport, mounted next to the
219
- widget to give a Storybook-grade dev loop.
220
-
221
- ## Local Development
222
-
223
- From the workspace root:
114
+ From the monorepo root:
224
115
 
225
116
  ```bash
226
117
  pnpm --filter grist-widget-sdk build
227
- pnpm --filter grist-widget-sdk dev
228
118
  pnpm --filter grist-widget-sdk test
229
- ```
230
-
231
- ## Pre-Commit / Pre-Publish Checks
232
-
233
- ```bash
234
- pnpm --filter grist-widget-sdk build
235
119
  pnpm --filter grist-widget-sdk exec tsc --noEmit
236
- pnpm --filter grist-widget-sdk exec vitest run
237
- pnpm dlx @arethetypeswrong/cli -P packages/core
238
120
  ```
239
-
240
- If no tests exist yet:
241
-
242
- ```bash
243
- pnpm --filter grist-widget-sdk exec vitest run --passWithNoTests
244
- ```
245
-
246
- ## Notes
247
-
248
- - This package targets pure TypeScript/React usage (no Next.js dependency required).
249
- - ATTW may report a `node10` warning for the `grist-widget-sdk/emulator` subpath. This is expected for modern `exports`-based packages and is non-blocking for Node 16+ and modern bundlers.