create-grist-widget 0.3.26 → 0.3.28
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/README.md +25 -7
- package/bin/create-grist-widget.mjs +125 -9
- package/bin/grist-widget-deploy.mjs +92 -0
- package/examples/reads-demo/WidgetApp.tsx +2 -1
- package/examples/safe-parse/WidgetApp.tsx +2 -1
- package/manifest.json +2 -2
- package/package.json +3 -2
- package/{template/scripts → src}/deploy.mjs +13 -81
- package/src/git-gate.mjs +2 -2
- package/src/upgrade.mjs +562 -0
- package/template/.github/dependabot.yml +5 -4
- package/template/.github/workflows/deploy.yml +32 -11
- package/template/AGENTS.md +36 -11
- package/template/README.md +19 -13
- package/template/package.json +2 -1
- package/template/src/app/App.smoke.test.tsx +20 -0
- package/template/src/main.tsx +1 -1
- package/template/vite.config.ts +1 -1
- package/vendor/compose-example.mjs +23 -20
- /package/template/src/{App.test.tsx → app/App.test.tsx} +0 -0
- /package/template/src/{App.tsx → app/App.tsx} +0 -0
- /package/template/src/{grist-types.example.ts → app/grist-types.example.ts} +0 -0
|
@@ -21,12 +21,16 @@ name: Deploy widget to GitHub Pages
|
|
|
21
21
|
# - push to dev -> dev channel: mutable /<repo>/dev/
|
|
22
22
|
# with version.json + a self-reload snippet, for live review inside Grist.
|
|
23
23
|
#
|
|
24
|
-
# All logic lives in
|
|
24
|
+
# All logic lives in the `grist-widget-deploy` command of create-grist-widget,
|
|
25
|
+
# a devDependency (node builtins only), so deploy fixes arrive as dependency
|
|
26
|
+
# updates.
|
|
25
27
|
#
|
|
26
28
|
# Two jobs, so no dependency code ever runs next to a write token. `build`
|
|
27
29
|
# installs and builds (Vite and its plugins are third-party code) with a
|
|
28
30
|
# read-only token, and hands dist/ over as an artifact. `publish` holds
|
|
29
|
-
# `contents: write
|
|
31
|
+
# `contents: write`: it installs with --ignore-scripts, so no dependency code
|
|
32
|
+
# runs, and then runs grist-widget-deploy alone, at the version the lockfile
|
|
33
|
+
# pins.
|
|
30
34
|
#
|
|
31
35
|
# One-time setup this workflow can't do for you: enable Pages (Settings ->
|
|
32
36
|
# Pages -> Source: Deploy from a branch -> gh-pages -> / (root); this workflow
|
|
@@ -53,7 +57,7 @@ on:
|
|
|
53
57
|
# Nothing at workflow level: each job states what it needs.
|
|
54
58
|
permissions: {}
|
|
55
59
|
|
|
56
|
-
# Serialize deploys onto the single shared gh-pages branch. deploy
|
|
60
|
+
# Serialize deploys onto the single shared gh-pages branch. grist-widget-deploy
|
|
57
61
|
# additionally rebases-and-retries on push, so interleaving runs never clobber.
|
|
58
62
|
concurrency:
|
|
59
63
|
group: pages-gh-pages
|
|
@@ -130,7 +134,7 @@ jobs:
|
|
|
130
134
|
REF: ${{ github.ref }}
|
|
131
135
|
FORCE: ${{ github.event.inputs.force == 'true' && '--force' || '' }}
|
|
132
136
|
run: |
|
|
133
|
-
PLAN=$(
|
|
137
|
+
PLAN=$(node_modules/.bin/grist-widget-deploy plan \
|
|
134
138
|
--site site --repo "$REPO_NAME" \
|
|
135
139
|
--event "$EVENT_NAME" \
|
|
136
140
|
--ref "$REF" \
|
|
@@ -179,16 +183,26 @@ jobs:
|
|
|
179
183
|
REF: ${{ github.ref }}
|
|
180
184
|
steps:
|
|
181
185
|
# Credentials stay persisted here: the first-release reset below pushes
|
|
182
|
-
# with them.
|
|
186
|
+
# with them. No dependency code runs in this job (see the install step).
|
|
183
187
|
- name: Checkout
|
|
184
188
|
uses: actions/checkout@v7
|
|
185
189
|
|
|
190
|
+
- name: Setup pnpm
|
|
191
|
+
uses: pnpm/action-setup@v6
|
|
192
|
+
|
|
186
193
|
- name: Setup Node
|
|
187
194
|
uses: actions/setup-node@v7
|
|
188
195
|
with:
|
|
189
196
|
node-version: 22
|
|
190
197
|
package-manager-cache: false
|
|
191
198
|
|
|
199
|
+
# Only for grist-widget-deploy, at the version (and integrity hash) the
|
|
200
|
+
# lockfile pins. --ignore-scripts: no dependency's install script runs,
|
|
201
|
+
# and the steps below call the bin directly, never through `pnpm
|
|
202
|
+
# exec`/`pnpm run`, whose dependency check may reinstall with scripts.
|
|
203
|
+
- name: Install the deploy command
|
|
204
|
+
run: pnpm install --ignore-scripts
|
|
205
|
+
|
|
192
206
|
- name: Clone gh-pages into ./site
|
|
193
207
|
env:
|
|
194
208
|
GH_TOKEN: ${{ github.token }}
|
|
@@ -219,7 +233,7 @@ jobs:
|
|
|
219
233
|
SHA="$(git rev-parse --short HEAD)"
|
|
220
234
|
VERSION_ARG=""
|
|
221
235
|
[ -n "$VERSION" ] && VERSION_ARG="--version $VERSION"
|
|
222
|
-
|
|
236
|
+
node_modules/.bin/grist-widget-deploy place \
|
|
223
237
|
--site site --repo "$REPO_NAME" \
|
|
224
238
|
--channel "$CONTEXT" $VERSION_ARG \
|
|
225
239
|
--dist dist \
|
|
@@ -237,13 +251,13 @@ jobs:
|
|
|
237
251
|
if [ "$CONTEXT" = "release" ]; then
|
|
238
252
|
UPDATE_VERSIONS="--update-versions"
|
|
239
253
|
fi
|
|
240
|
-
|
|
254
|
+
node_modules/.bin/grist-widget-deploy finalize \
|
|
241
255
|
--site site --repo "$REPO_NAME" $UPDATE_VERSIONS --push \
|
|
242
256
|
--commit-message "deploy: ${CONTEXT} @ ${SHA_FULL}"
|
|
243
257
|
|
|
244
258
|
- name: Reset repo to a clean first-release state
|
|
245
259
|
if: needs.build.outputs.context == 'release' && needs.build.outputs.firstRelease == 'true'
|
|
246
|
-
run:
|
|
260
|
+
run: node_modules/.bin/grist-widget-deploy reset-branches --repo "$REPO_NAME"
|
|
247
261
|
|
|
248
262
|
cleanup:
|
|
249
263
|
# When the `dev` branch is deleted, remove /dev/ so the dev URL retires
|
|
@@ -253,15 +267,22 @@ jobs:
|
|
|
253
267
|
permissions:
|
|
254
268
|
contents: write
|
|
255
269
|
steps:
|
|
256
|
-
- name: Checkout (default branch — for
|
|
270
|
+
- name: Checkout (default branch — for its pinned grist-widget-deploy)
|
|
257
271
|
uses: actions/checkout@v7
|
|
258
272
|
|
|
273
|
+
- name: Setup pnpm
|
|
274
|
+
uses: pnpm/action-setup@v6
|
|
275
|
+
|
|
259
276
|
- name: Setup Node
|
|
260
277
|
uses: actions/setup-node@v7
|
|
261
278
|
with:
|
|
262
279
|
node-version: 22
|
|
263
280
|
package-manager-cache: false
|
|
264
281
|
|
|
282
|
+
# As in `publish`: no dependency scripts run, and the bin is called directly.
|
|
283
|
+
- name: Install the deploy command
|
|
284
|
+
run: pnpm install --ignore-scripts
|
|
285
|
+
|
|
265
286
|
- name: Clone gh-pages into ./site (skip if it does not exist)
|
|
266
287
|
id: clone
|
|
267
288
|
env:
|
|
@@ -283,7 +304,7 @@ jobs:
|
|
|
283
304
|
env:
|
|
284
305
|
DELETED_REF: ${{ github.event.ref }}
|
|
285
306
|
run: |
|
|
286
|
-
|
|
287
|
-
|
|
307
|
+
node_modules/.bin/grist-widget-deploy remove --site site
|
|
308
|
+
node_modules/.bin/grist-widget-deploy finalize \
|
|
288
309
|
--site site --push \
|
|
289
310
|
--commit-message "cleanup: retire dev URL for ${DELETED_REF}"
|
package/template/AGENTS.md
CHANGED
|
@@ -11,21 +11,42 @@
|
|
|
11
11
|
|
|
12
12
|
## The widget contract
|
|
13
13
|
|
|
14
|
-
`src/
|
|
14
|
+
The widget lives in `src/app/`. Its entry point, `src/app/App.tsx`, has three exports:
|
|
15
15
|
|
|
16
16
|
```tsx
|
|
17
17
|
export const GRIST_OPTIONS: UseGristOptions = { requiredAccess: "read table" }
|
|
18
18
|
export const WIDGET_METADATA = { title: "...", description: "..." } as const
|
|
19
|
-
export function App() {
|
|
19
|
+
export default function App() {
|
|
20
20
|
const w = useGrist()
|
|
21
21
|
// ...
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
`GRIST_OPTIONS` may live in a file of its own, as long as `App.tsx` re-exports it: the
|
|
26
|
+
template's `src/main.tsx` imports both from `./app/App`.
|
|
27
|
+
|
|
25
28
|
`src/main.tsx` is one call, `mountGristWidget({ App, options: GRIST_OPTIONS, dev })` from
|
|
26
29
|
`grist-widget-sdk/app`: inside Grist it renders `<App />` in `GristWidgetProvider` and
|
|
27
30
|
`GristBoundary`, with the SDK's alerts and Grist's light/dark theme. Access level, column
|
|
28
|
-
mapping, and rendering all live in `src/App.tsx` — that's the
|
|
31
|
+
mapping, and rendering all live in `src/app/App.tsx` — that's the file to edit.
|
|
32
|
+
|
|
33
|
+
### What is the template's, what is the widget's
|
|
34
|
+
|
|
35
|
+
- **The template's:** `src/main.tsx`, `src/components/theme-provider.tsx`,
|
|
36
|
+
`src/lib/utils.ts`, `src/vite-env.d.ts`, the workflows and Dependabot config,
|
|
37
|
+
`vite.config.ts`, `tsconfig.json`, `eslint.config.js`, the Prettier config,
|
|
38
|
+
`components.json`, `CLAUDE.md` and this file.
|
|
39
|
+
Leave them as they are: `pnpm exec create-grist-widget upgrade` rewrites them to the
|
|
40
|
+
template of the `create-grist-widget` version the widget depends on
|
|
41
|
+
(<https://gristwidgets.com/docs/cli/upgrading>), and a local edit would be lost.
|
|
42
|
+
- **The widget's:** everything else. `src/app/` holds its code and tests, the smoke test
|
|
43
|
+
included: put every new component, hook, helper and asset there. `src/components/ui/`
|
|
44
|
+
holds the shadcn primitives, where `shadcn add` writes (next to any other registry
|
|
45
|
+
folder it installs), and `src/index.css` the theme. `tsconfig.app.json` and
|
|
46
|
+
`tsconfig.node.json` keep the widget's own paths and types (their compiler settings come
|
|
47
|
+
from `grist-widget-sdk`). `README.md`, `index.html`, `public/` and
|
|
48
|
+
`pnpm-workspace.yaml` are the widget's too, and so is `package.json`, apart from the
|
|
49
|
+
scripts and dependency versions the template sets.
|
|
29
50
|
|
|
30
51
|
## Operating contract
|
|
31
52
|
|
|
@@ -37,7 +58,7 @@ mapping, and rendering all live in `src/App.tsx` — that's the one file to edit
|
|
|
37
58
|
real Grist, so a fake local preview would teach the wrong lesson. Trust `pnpm test`
|
|
38
59
|
(headless, real assertions) for local feedback, and the `dev`-branch loop below for real
|
|
39
60
|
behavior — don't build or suggest a seeded `pnpm dev` preview.
|
|
40
|
-
3. **Widget changes get a test.** `src/App.test.tsx` uses `renderWithGrist` + `presets` from
|
|
61
|
+
3. **Widget changes get a test.** `src/app/App.test.tsx` uses `renderWithGrist` + `presets` from
|
|
41
62
|
`grist-widget-sdk/emulator/testing` — no browser or real Grist doc needed. Run `pnpm test`.
|
|
42
63
|
4. **Always develop on `dev`, release by merging to `main`.** Commit and push to `dev` for
|
|
43
64
|
every change — it auto-deploys a live preview at `.../dev/` that self-reloads inside an
|
|
@@ -58,17 +79,19 @@ mapping, and rendering all live in `src/App.tsx` — that's the one file to edit
|
|
|
58
79
|
| Format | `pnpm format` |
|
|
59
80
|
| Type-check | `pnpm typecheck` |
|
|
60
81
|
| Preview a production build | `pnpm preview` |
|
|
82
|
+
| Bring the template's files up to date (on `dev`, after updating create-grist-widget) | `pnpm exec create-grist-widget upgrade` |
|
|
61
83
|
|
|
62
84
|
## Path map
|
|
63
85
|
|
|
64
86
|
| Concept | Path |
|
|
65
87
|
| --- | --- |
|
|
66
|
-
| The widget | `src/
|
|
67
|
-
|
|
|
88
|
+
| The widget (everything in it is the widget's own) | `src/app/` |
|
|
89
|
+
| Its entry point (GRIST_OPTIONS, WIDGET_METADATA, App) | `src/app/App.tsx` |
|
|
90
|
+
| Widget test (the pattern to copy for new tests) | `src/app/App.test.tsx` |
|
|
91
|
+
| Smoke test (one smokeTestWidget call; CI runs it on SDK updates) | `src/app/App.smoke.test.tsx` |
|
|
68
92
|
| Grist wiring (mountGristWidget, inside vs. outside Grist) | `src/main.tsx` |
|
|
69
93
|
| ui primitives | `src/components/ui/` |
|
|
70
|
-
| Deploy workflow | `.github/workflows/deploy.yml` |
|
|
71
|
-
| Deploy script (version guard, channel logic) | `scripts/deploy.mjs` |
|
|
94
|
+
| Deploy workflow (runs grist-widget-deploy from the create-grist-widget devDependency: version guard, channel logic) | `.github/workflows/deploy.yml` |
|
|
72
95
|
| Pull request checks (lint, test, build) | `.github/workflows/ci.yml` |
|
|
73
96
|
| Dependency update PRs (weekly, against the dev branch) | `.github/dependabot.yml` |
|
|
74
97
|
| Full walkthrough (dev loop, deploying, embedding) | `README.md` |
|
|
@@ -95,10 +118,12 @@ Full detail, including the two live URLs (`/latest/` and `/v<version>/`): `READM
|
|
|
95
118
|
template works (see above); `pnpm test` is the trusted local loop, real Grist via `/dev/`
|
|
96
119
|
is the trusted real one
|
|
97
120
|
- Merging `dev` into `main` without bumping `package.json`'s `version` first
|
|
121
|
+
- Adding widget code outside `src/app/`, or editing the template's files to change the
|
|
122
|
+
widget (see the widget contract above)
|
|
98
123
|
- Adding `minimumReleaseAgeExclude` entries to `pnpm-workspace.yaml` to get an install or a
|
|
99
|
-
check green, or deleting the
|
|
124
|
+
check green, or deleting the ones pnpm wrote for `grist-widget-sdk` and `create-grist-widget` on release day (see
|
|
100
125
|
`README.md`): pnpm's 24 h age check protects the widget's supply chain
|
|
101
126
|
- Merging a Dependabot pull request whose `ci.yml` checks are red, or retargeting one at
|
|
102
|
-
`main`: updates go through `dev` like any other change
|
|
103
|
-
|
|
127
|
+
`main`: updates go through `dev` like any other change, template-file updates
|
|
128
|
+
(`pnpm exec create-grist-widget upgrade`) included —
|
|
104
129
|
<https://gristwidgets.com/docs/cli/upgrading>
|
package/template/README.md
CHANGED
|
@@ -12,12 +12,14 @@ Opened outside a Grist iframe, the page is never a live preview of the widget it
|
|
|
12
12
|
|
|
13
13
|
When actually embedded, the widget shows connection state only when something is wrong: until Grist is connected, or if the connection is lost or fails, `GristBoundary` replaces the widget with a connecting / unavailable / error screen (the error one with a retry button); once the widget is showing, the SDK's alerts report what needs attention (column mapping, access level, a failed write…) above it. Nothing is drawn over the widget while all is well. Pass your own `Alerts` component to `mountGristWidget()` to render them differently, or `Alerts: null` for none.
|
|
14
14
|
|
|
15
|
-
`src/App.tsx
|
|
15
|
+
The widget lives in `src/app/`: `App.tsx`, and any components, hooks, helpers or assets it needs. `src/components/ui/` (shadcn primitives) and `src/index.css` (the theme) are yours too. `src/main.tsx`, the workflows and the Vite and ESLint config are the template's (`AGENTS.md` lists them): leave them as they are: `pnpm exec create-grist-widget upgrade`, run on `dev`, rewrites them when `create-grist-widget` is updated ([Upgrading a widget](https://gristwidgets.com/docs/cli/upgrading)).
|
|
16
|
+
|
|
17
|
+
`src/app/App.tsx` uses `useGrist<TaskRow, TaskMapped>()` for the selected row (`w.record`, `w.mode`) and remounts the UI with `key={rowKey}` when the row changes. It ships with two mapped columns (`title`, `done`) and an editable form that saves back via `w.table.update(...)`/`w.mapBack(...)` — the smallest real starting point, not a placeholder. See `src/app/grist-types.example.ts` for typing patterns.
|
|
16
18
|
|
|
17
19
|
- **ESLint** blocks direct `grist` global usage in `src/` — use the SDK only.
|
|
18
|
-
- Edit `GRIST_OPTIONS.columns` in `App.tsx` to change the required columns (or remove the array entirely to read raw, unmapped records instead); `mountGristWidget()` gates the widget on `canRender` whenever columns are declared, so it renders once they're mapped.
|
|
20
|
+
- Edit `GRIST_OPTIONS.columns` in `src/app/App.tsx` to change the required columns (or remove the array entirely to read raw, unmapped records instead); `mountGristWidget()` gates the widget on `canRender` whenever columns are declared, so it renders once they're mapped.
|
|
19
21
|
|
|
20
|
-
Run `pnpm test`
|
|
22
|
+
Run `pnpm test`. `src/app/App.smoke.test.tsx` is the smoke test, one `smokeTestWidget()` call that checks the widget renders inside an emulated Grist with every declared column mapped, and that CI runs on every pull request; see `src/app/App.test.tsx` for the pattern (`renderWithGrist` + `presets` from `grist-widget-sdk/emulator/testing`, no browser or real Grist doc needed). Full guide: [Testing](https://gristwidgets.com/docs/emulator/testing).
|
|
21
23
|
|
|
22
24
|
**Monorepo dev:** this template resolves `grist-widget-sdk` from `packages/core/dist` (like the other widgets), not from SDK source. After changing the SDK, run `pnpm prebuild` or `pnpm --filter grist-widget-sdk build` before `pnpm dev`.
|
|
23
25
|
|
|
@@ -43,11 +45,12 @@ pnpm, even under `npm create`, unless it was run with `--no-install` or
|
|
|
43
45
|
without pnpm installed. Versions are pinned with their integrity hashes, CI
|
|
44
46
|
installs exactly that, and Dependabot updates it. A `package-lock.json` isn't
|
|
45
47
|
used: the workflows install with pnpm. pnpm 11 won't install a version
|
|
46
|
-
published less than 24 hours ago. So a widget scaffolded on the day of
|
|
47
|
-
release also gets
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
until the version is a day old.
|
|
48
|
+
published less than 24 hours ago. So a widget scaffolded on the day of a
|
|
49
|
+
release also gets `minimumReleaseAgeExclude` entries in `pnpm-workspace.yaml`,
|
|
50
|
+
written by pnpm itself, for the `grist-widget-sdk` and `create-grist-widget`
|
|
51
|
+
versions it was made with, and nothing else. Keep them: without them, every
|
|
52
|
+
install (yours and CI's) rejects the lockfile until the version is a day old.
|
|
53
|
+
After that the entries are harmless.
|
|
51
54
|
|
|
52
55
|
### Live preview
|
|
53
56
|
|
|
@@ -68,8 +71,10 @@ directory, same as `scripts/smoke/create-widget.sh` does.
|
|
|
68
71
|
|
|
69
72
|
## Deployment
|
|
70
73
|
|
|
71
|
-
A bundled GitHub Actions workflow (`.github/workflows/deploy.yml
|
|
72
|
-
`
|
|
74
|
+
A bundled GitHub Actions workflow (`.github/workflows/deploy.yml`, which runs
|
|
75
|
+
the `grist-widget-deploy` command of the `create-grist-widget` devDependency)
|
|
76
|
+
publishes this widget to **your own** GitHub Pages. Fixes to the deploy logic
|
|
77
|
+
arrive as updates of that devDependency.
|
|
73
78
|
|
|
74
79
|
### The workflow: always develop on `dev`, release by merging to `main`
|
|
75
80
|
|
|
@@ -132,9 +137,10 @@ dependencies (`grist-widget-sdk` included) and the workflows' GitHub Actions,
|
|
|
132
137
|
and `.github/workflows/ci.yml` lints, tests and builds every pull request.
|
|
133
138
|
Merge a green one into `dev`, check `/dev/` inside Grist, then release it
|
|
134
139
|
like any change (bump the version, merge `dev` into `main`). Changes to the
|
|
135
|
-
template's own files (this pipeline, the build config)
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
template's own files (this pipeline, the build config) arrive with
|
|
141
|
+
`create-grist-widget`'s updates: after merging one, run
|
|
142
|
+
`pnpm exec create-grist-widget upgrade` on `dev` (see
|
|
143
|
+
[Upgrading a widget](https://gristwidgets.com/docs/cli/upgrading)).
|
|
138
144
|
|
|
139
145
|
**One-time setup** (the workflow can't do this part for you):
|
|
140
146
|
|
package/template/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@tailwindcss/vite": "^4.3.3",
|
|
18
18
|
"class-variance-authority": "^0.7.1",
|
|
19
19
|
"clsx": "^2.1.1",
|
|
20
|
-
"grist-widget-sdk": "^0.3.
|
|
20
|
+
"grist-widget-sdk": "^0.3.27",
|
|
21
21
|
"lucide-react": "^1.61.0",
|
|
22
22
|
"radix-ui": "^1.8.1",
|
|
23
23
|
"react": "^19.3.1",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@types/react": "^19.3.0",
|
|
36
36
|
"@types/react-dom": "^19.3.0",
|
|
37
37
|
"@vitejs/plugin-react": "^6.1.2",
|
|
38
|
+
"create-grist-widget": "^0.3.28",
|
|
38
39
|
"eslint": "^10.13.0",
|
|
39
40
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
40
41
|
"eslint-plugin-react-refresh": "^0.5.7",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test: the widget renders inside an emulated Grist, mounted as
|
|
3
|
+
* `main.tsx` mounts it, with every column `GRIST_OPTIONS` declares mapped.
|
|
4
|
+
*
|
|
5
|
+
* It only reads `GRIST_OPTIONS`, so it stays valid as the widget changes,
|
|
6
|
+
* and it improves with each SDK release. `.github/workflows/ci.yml` runs it
|
|
7
|
+
* on every pull request, Dependabot's `grist-widget-sdk` updates included:
|
|
8
|
+
* an SDK release that breaks this widget turns that PR red before the merge.
|
|
9
|
+
*
|
|
10
|
+
* It is the widget's own file: a widget using a browser API jsdom lacks
|
|
11
|
+
* (WebGL, say) stubs it here, with `vi.mock`. Widget-specific assertions go
|
|
12
|
+
* in their own tests (see `App.test.tsx`).
|
|
13
|
+
*/
|
|
14
|
+
import { it } from "vitest"
|
|
15
|
+
import { smokeTestWidget } from "grist-widget-sdk/emulator/testing"
|
|
16
|
+
|
|
17
|
+
import App, { GRIST_OPTIONS } from "./App"
|
|
18
|
+
|
|
19
|
+
it("renders inside Grist with every declared column mapped", () =>
|
|
20
|
+
smokeTestWidget({ App, options: GRIST_OPTIONS }))
|
package/template/src/main.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mountGristWidget } from "grist-widget-sdk/app"
|
|
2
2
|
|
|
3
3
|
import "./index.css"
|
|
4
|
-
import App, { GRIST_OPTIONS } from "./App.tsx"
|
|
4
|
+
import App, { GRIST_OPTIONS } from "./app/App.tsx"
|
|
5
5
|
|
|
6
6
|
// Inside Grist: the widget, behind the SDK's connection screens and alerts,
|
|
7
7
|
// in Grist's light/dark theme. Opened anywhere else (`pnpm dev`, a deployed
|
package/template/vite.config.ts
CHANGED
|
@@ -28,7 +28,7 @@ export default defineConfig({
|
|
|
28
28
|
environment: "jsdom",
|
|
29
29
|
// Most examples/<id>/ don't ship their own WidgetApp.test.tsx yet (it's
|
|
30
30
|
// optional, per docs/planning/2026-08-execution-plan.md §3) --
|
|
31
|
-
// overlayExampleSrc correctly deletes the template's own App.test.tsx
|
|
31
|
+
// overlayExampleSrc correctly deletes the template's own app/App.test.tsx
|
|
32
32
|
// when swapping in one that has none (it would otherwise assert against
|
|
33
33
|
// content that's no longer there), so `create --example <id>` / `use
|
|
34
34
|
// <id>` routinely leave a project with zero test files. Without this,
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import {
|
|
30
30
|
cpSync,
|
|
31
31
|
existsSync,
|
|
32
|
+
mkdirSync,
|
|
32
33
|
readFileSync,
|
|
33
34
|
readdirSync,
|
|
34
35
|
rmSync,
|
|
@@ -92,18 +93,20 @@ export function createComposer({ templateDir, examplesDir, uiDir, manifestPath }
|
|
|
92
93
|
}
|
|
93
94
|
const manifestEntry = loadManifestEntry(exampleId)
|
|
94
95
|
const srcDir = join(destDir, "src")
|
|
96
|
+
const appDir = join(srcDir, "app")
|
|
95
97
|
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
// src/app/ is the widget's own folder (see the template's AGENTS.md), so
|
|
99
|
+
// the example replaces it whole: nothing from the previous widget lingers,
|
|
100
|
+
// its test included, which would assert against content no longer there.
|
|
101
|
+
// The template's files around it (main.tsx, components/ui, lib,
|
|
102
|
+
// index.css) stay as they are.
|
|
103
|
+
rmSync(appDir, { recursive: true, force: true })
|
|
104
|
+
mkdirSync(appDir, { recursive: true })
|
|
105
|
+
|
|
106
|
+
// The template's smoke test comes back with any example: it only reads
|
|
107
|
+
// GRIST_OPTIONS, so a widget started from an example is smoke-tested too.
|
|
108
|
+
const smokeTest = join(templateDir, "src/app/App.smoke.test.tsx")
|
|
109
|
+
if (existsSync(smokeTest)) cpSync(smokeTest, join(appDir, "App.smoke.test.tsx"))
|
|
107
110
|
|
|
108
111
|
// The host contract's only mismatch: examples export `WidgetApp` (no
|
|
109
112
|
// default export), the template's main.tsx does a default-only import.
|
|
@@ -111,23 +114,18 @@ export function createComposer({ templateDir, examplesDir, uiDir, manifestPath }
|
|
|
111
114
|
// stays untouched under examples/) — same idiom as deploy.mjs's
|
|
112
115
|
// identityEdits.
|
|
113
116
|
const widgetAppSrc = readFileSync(join(exampleDir, "WidgetApp.tsx"), "utf8")
|
|
114
|
-
writeFileSync(join(
|
|
117
|
+
writeFileSync(join(appDir, "App.tsx"), `${widgetAppSrc}\nexport default WidgetApp\n`)
|
|
115
118
|
|
|
116
119
|
const testPath = join(exampleDir, "WidgetApp.test.tsx")
|
|
117
|
-
const destTestPath = join(srcDir, "App.test.tsx")
|
|
118
120
|
if (existsSync(testPath)) {
|
|
119
|
-
writeFileSync(
|
|
120
|
-
} else if (existsSync(destTestPath)) {
|
|
121
|
-
// The template's own test (or a previous example's) asserts against
|
|
122
|
-
// content that's no longer there once App.tsx is someone else's widget.
|
|
123
|
-
rmSync(destTestPath)
|
|
121
|
+
writeFileSync(join(appDir, "App.test.tsx"), readFileSync(testPath, "utf8"))
|
|
124
122
|
}
|
|
125
123
|
|
|
126
124
|
// Every other file living alongside WidgetApp.tsx (e.g. spreadsheet-grid's 5
|
|
127
125
|
// sibling components) travels with it, verbatim.
|
|
128
126
|
for (const entry of readdirSync(exampleDir, { withFileTypes: true })) {
|
|
129
127
|
if (entry.name === "WidgetApp.tsx" || entry.name === "WidgetApp.test.tsx") continue
|
|
130
|
-
cpSync(join(exampleDir, entry.name), join(
|
|
128
|
+
cpSync(join(exampleDir, entry.name), join(appDir, entry.name), { recursive: true })
|
|
131
129
|
}
|
|
132
130
|
|
|
133
131
|
// `@/components/ui/*` the template doesn't already vendor (per the
|
|
@@ -175,11 +173,14 @@ export function createComposer({ templateDir, examplesDir, uiDir, manifestPath }
|
|
|
175
173
|
* `workspace:^` (meaningless outside the pnpm workspace) — a `file:`
|
|
176
174
|
* reference to either `packages/core` (workspace source, already built), a
|
|
177
175
|
* packed tarball, or (for the CLI's `create`) a real published semver range.
|
|
176
|
+
* `cliDependency` does the same for the template's `create-grist-widget`
|
|
177
|
+
* devDependency (its deploy bin); without one, the devDependency is dropped,
|
|
178
|
+
* since a composed example is only typechecked, built and tested here.
|
|
178
179
|
*
|
|
179
180
|
* For composing *many* examples against one shared `pnpm install`, use
|
|
180
181
|
* {@link overlayExampleSrc} directly instead (see its own doc comment).
|
|
181
182
|
*/
|
|
182
|
-
function composeExample({ exampleId, destDir, sdkDependency }) {
|
|
183
|
+
function composeExample({ exampleId, destDir, sdkDependency, cliDependency }) {
|
|
183
184
|
rmSync(destDir, { recursive: true, force: true })
|
|
184
185
|
cpSync(templateDir, destDir, {
|
|
185
186
|
recursive: true,
|
|
@@ -192,6 +193,8 @@ export function createComposer({ templateDir, examplesDir, uiDir, manifestPath }
|
|
|
192
193
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
|
|
193
194
|
pkg.name = exampleId
|
|
194
195
|
pkg.dependencies["grist-widget-sdk"] = sdkDependency
|
|
196
|
+
if (cliDependency) pkg.devDependencies["create-grist-widget"] = cliDependency
|
|
197
|
+
else delete pkg.devDependencies["create-grist-widget"]
|
|
195
198
|
Object.assign(pkg.dependencies, EXTRA_DEPENDENCIES[exampleId] ?? {})
|
|
196
199
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
197
200
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|