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.
- package/CHANGELOG.md +47 -0
- package/README.md +79 -208
- package/dist/app.cjs +186 -65
- package/dist/app.cjs.map +1 -1
- package/dist/app.d.cts +5 -100
- package/dist/app.d.ts +5 -100
- package/dist/app.js +4 -2
- package/dist/{chunk-O7NY46QY.js → chunk-MAPAWVQC.js} +88 -20
- package/dist/chunk-MAPAWVQC.js.map +1 -0
- package/dist/{chunk-B5ZZNLVS.js → chunk-OXA2VITC.js} +5 -1
- package/dist/chunk-OXA2VITC.js.map +1 -0
- package/dist/{chunk-P6VTEKWO.js → chunk-P5I5MRC6.js} +86 -10
- package/dist/chunk-P5I5MRC6.js.map +1 -0
- package/dist/{use-grist-BK0eke9K.d.cts → define-widget-B1FIvI-h.d.cts} +179 -104
- package/dist/{use-grist-C5DMZ3wU.d.ts → define-widget-B9gEtJP0.d.ts} +179 -104
- package/dist/emulator/testing.cjs +2393 -2243
- package/dist/emulator/testing.cjs.map +1 -1
- package/dist/emulator/testing.d.cts +27 -9
- package/dist/emulator/testing.d.ts +27 -9
- package/dist/emulator/testing.js +75 -55
- package/dist/emulator/testing.js.map +1 -1
- package/dist/emulator.cjs +4 -0
- package/dist/emulator.cjs.map +1 -1
- package/dist/emulator.d.cts +3 -3
- package/dist/emulator.d.ts +3 -3
- package/dist/emulator.js +1 -1
- package/dist/index.cjs +133 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/mount-grist-widget-CkB-3iG_.d.ts +137 -0
- package/dist/mount-grist-widget-Dtelqao_.d.cts +137 -0
- package/package.json +1 -1
- package/dist/chunk-B5ZZNLVS.js.map +0 -1
- package/dist/chunk-O7NY46QY.js.map +0 -1
- 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
|
|
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
|
-
|
|
8
|
+
Documentation: <https://gristwidgets.com/docs>. To start a widget, use the
|
|
9
|
+
scaffold, which wires all of this up:
|
|
6
10
|
|
|
7
|
-
|
|
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`
|
|
12
|
-
| Wire-format dates
|
|
13
|
-
| `mapColumnNames
|
|
14
|
-
| No test harness | `renderWithGrist`
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
39
|
+
## A widget
|
|
47
40
|
|
|
48
41
|
```tsx
|
|
49
|
-
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
81
|
-
}
|
|
63
|
+
mountGristWidget(entry, { dev: import.meta.env.DEV })
|
|
82
64
|
```
|
|
83
65
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 {
|
|
96
|
-
import {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
112
|
+
## Developing this package
|
|
151
113
|
|
|
152
|
-
|
|
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.
|