create-grist-widget 0.3.25 → 0.3.27
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/bin/create-grist-widget.mjs +1 -1
- 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/examples/spreadsheet-grid/GristDataSheetGrid.tsx +1 -1
- package/examples/spreadsheet-grid/GristSchemaToolbar.tsx +4 -4
- package/manifest.json +2 -2
- package/package.json +3 -2
- package/{template/scripts → src}/deploy.mjs +14 -85
- package/template/.github/dependabot.yml +5 -4
- package/template/.github/workflows/deploy.yml +32 -11
- package/template/AGENTS.md +9 -8
- package/template/README.md +15 -12
- package/template/package.json +2 -1
- package/template/src/App.smoke.test.tsx +17 -0
- package/template/src/App.test.tsx +6 -13
- package/template/src/components/theme-provider.tsx +5 -229
- package/template/src/main.tsx +7 -53
- package/vendor/compose-example.mjs +7 -2
- package/template/src/components/grist-sdk-alerts.tsx +0 -80
- package/template/src/components/local-dev-notice.tsx +0 -55
- package/template/src/components/release-info.tsx +0 -111
- package/template/src/lib/showcase-routing.ts +0 -59
- package/template/src/lib/showcase-versions.ts +0 -63
|
@@ -223,7 +223,7 @@ function printAgeExceptionsNote(ageExceptions) {
|
|
|
223
223
|
`\nNote: ${ageExceptions.join(", ")} came out less than 24 hours ago. pnpm only installs versions` +
|
|
224
224
|
"\nthat recent when told to, so it listed them under minimumReleaseAgeExclude in pnpm-workspace.yaml." +
|
|
225
225
|
"\nThat entry is committed: without it, every later install (yours and CI's) rejects the lockfile." +
|
|
226
|
-
"\nIt only covers the SDK
|
|
226
|
+
"\nIt only covers the SDK and CLI versions this scaffold was made with, and is moot once a day old."
|
|
227
227
|
)
|
|
228
228
|
}
|
|
229
229
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// grist-widget-deploy — the GitHub Pages deploy steps a widget's
|
|
3
|
+
// .github/workflows/deploy.yml runs (plan, place, remove, finalize,
|
|
4
|
+
// reset-branches). See src/deploy.mjs for what each one does.
|
|
5
|
+
//
|
|
6
|
+
// The publish job runs this next to a `contents: write` token, after an
|
|
7
|
+
// install with --ignore-scripts, so this file and src/deploy.mjs must import
|
|
8
|
+
// node builtins only: no other dependency code may run there
|
|
9
|
+
// (tests/deploy-imports.test.mjs).
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
finalize,
|
|
13
|
+
placeTarget,
|
|
14
|
+
plan,
|
|
15
|
+
removeDevDir,
|
|
16
|
+
resetBranchesIfFirstRelease,
|
|
17
|
+
} from "../src/deploy.mjs"
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const out = { _: [] }
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
const a = argv[i]
|
|
23
|
+
if (a.startsWith("--")) {
|
|
24
|
+
const key = a.slice(2)
|
|
25
|
+
const next = argv[i + 1]
|
|
26
|
+
if (next === undefined || next.startsWith("--")) out[key] = true
|
|
27
|
+
else {
|
|
28
|
+
out[key] = next
|
|
29
|
+
i++
|
|
30
|
+
}
|
|
31
|
+
} else out._.push(a)
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [cmd, ...rest] = process.argv.slice(2)
|
|
37
|
+
const args = parseArgs(rest)
|
|
38
|
+
|
|
39
|
+
switch (cmd) {
|
|
40
|
+
case "plan": {
|
|
41
|
+
const result = plan(args.site, args.repo, args.event, args.ref, {
|
|
42
|
+
force: !!args.force,
|
|
43
|
+
})
|
|
44
|
+
process.stdout.write(JSON.stringify(result))
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
case "place": {
|
|
48
|
+
const res = placeTarget({
|
|
49
|
+
siteDir: args.site,
|
|
50
|
+
channel: args.channel,
|
|
51
|
+
version: args.version,
|
|
52
|
+
distDir: args.dist,
|
|
53
|
+
sha: args.sha,
|
|
54
|
+
ref: args.ref,
|
|
55
|
+
repo: args.repo,
|
|
56
|
+
})
|
|
57
|
+
console.log("placed:", JSON.stringify(res))
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
case "remove": {
|
|
61
|
+
const res = removeDevDir(args.site)
|
|
62
|
+
console.log("remove:", JSON.stringify(res))
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
case "finalize": {
|
|
66
|
+
const res = finalize({
|
|
67
|
+
siteDir: args.site,
|
|
68
|
+
updateVersions: !!args["update-versions"],
|
|
69
|
+
push: !!args.push,
|
|
70
|
+
commitMessage: args["commit-message"],
|
|
71
|
+
repo: args.repo,
|
|
72
|
+
})
|
|
73
|
+
console.log("finalize:", JSON.stringify(res))
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
case "reset-branches": {
|
|
77
|
+
const res = resetBranchesIfFirstRelease(
|
|
78
|
+
args["repo-dir"] || ".",
|
|
79
|
+
args.repo,
|
|
80
|
+
undefined,
|
|
81
|
+
args["owner-repo"] || process.env.GITHUB_REPOSITORY
|
|
82
|
+
)
|
|
83
|
+
console.log("reset-branches:", JSON.stringify(res))
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
console.error(
|
|
88
|
+
`grist-widget-deploy: unknown subcommand "${cmd || "(none)"}". ` +
|
|
89
|
+
"Expected plan, place, remove, finalize or reset-branches."
|
|
90
|
+
)
|
|
91
|
+
process.exit(2)
|
|
92
|
+
}
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
} from "grist-widget-sdk"
|
|
8
8
|
|
|
9
9
|
export const GRIST_OPTIONS: UseGristOptions = {
|
|
10
|
-
|
|
10
|
+
// fetchTableRows and fetchRow read any table, which Grist only allows with full document access.
|
|
11
|
+
requiredAccess: "full",
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export const WIDGET_METADATA = {
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
type TaskRow = { Title: string; Due: Date | null }
|
|
11
11
|
|
|
12
12
|
export const GRIST_OPTIONS: UseGristOptions = {
|
|
13
|
-
|
|
13
|
+
// fetchTable reads any table, which Grist only allows with full document access.
|
|
14
|
+
requiredAccess: "full",
|
|
14
15
|
suppressAlerts: ["section-not-linked"],
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -320,7 +320,7 @@ export function GristDataSheetGrid({
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
// Sequence: deletes → updates → creates. Errors are tracked by the SDK
|
|
323
|
-
// and surfaced
|
|
323
|
+
// and surfaced by the SDK's alerts (mountGristWidget).
|
|
324
324
|
void (async () => {
|
|
325
325
|
try {
|
|
326
326
|
if (deletes.length > 0) await ops.destroy(deletes)
|
|
@@ -98,7 +98,7 @@ export function GristSchemaToolbar({
|
|
|
98
98
|
setNewColId("")
|
|
99
99
|
setNewType("Text")
|
|
100
100
|
} catch {
|
|
101
|
-
// Surfaced
|
|
101
|
+
// Surfaced by the SDK's alerts (mountGristWidget).
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -109,7 +109,7 @@ export function GristSchemaToolbar({
|
|
|
109
109
|
await w.applyActions([gristRenameColumnAction(tableId, oldId, newId)])
|
|
110
110
|
await schema.reload()
|
|
111
111
|
} catch {
|
|
112
|
-
/* surfaced
|
|
112
|
+
/* surfaced by the SDK's alerts */
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -118,7 +118,7 @@ export function GristSchemaToolbar({
|
|
|
118
118
|
await w.applyActions([gristModifyColumnAction(tableId, colId, { type })])
|
|
119
119
|
await schema.reload()
|
|
120
120
|
} catch {
|
|
121
|
-
/* surfaced
|
|
121
|
+
/* surfaced by the SDK's alerts */
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -128,7 +128,7 @@ export function GristSchemaToolbar({
|
|
|
128
128
|
await w.applyActions([gristRemoveColumnAction(tableId, colId)])
|
|
129
129
|
await schema.reload()
|
|
130
130
|
} catch {
|
|
131
|
-
/* surfaced
|
|
131
|
+
/* surfaced by the SDK's alerts */
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
package/manifest.json
CHANGED
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"id": "reads-demo",
|
|
135
135
|
"title": "Reads demo",
|
|
136
136
|
"description": "listTables, fetchTableRows, fetchRow, fetchSelectedRecord.",
|
|
137
|
-
"requiredAccess": "
|
|
137
|
+
"requiredAccess": "full",
|
|
138
138
|
"columns": [],
|
|
139
139
|
"allowSelectBy": false,
|
|
140
140
|
"hasCustomOptions": false,
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"id": "safe-parse",
|
|
161
161
|
"title": "Safe parse",
|
|
162
162
|
"description": "safeParseGristTableData with per-cell issues.",
|
|
163
|
-
"requiredAccess": "
|
|
163
|
+
"requiredAccess": "full",
|
|
164
164
|
"columns": [],
|
|
165
165
|
"allowSelectBy": false,
|
|
166
166
|
"hasCustomOptions": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-grist-widget",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.27",
|
|
4
4
|
"description": "Scaffold a new Grist custom widget from the grist-widget-sdk Vite template",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"node": "^22.22.0 || >=24.15.0"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
|
-
"create-grist-widget": "./bin/create-grist-widget.mjs"
|
|
11
|
+
"create-grist-widget": "./bin/create-grist-widget.mjs",
|
|
12
|
+
"grist-widget-deploy": "./bin/grist-widget-deploy.mjs"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"bin",
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
// Deploy publisher for GitHub Pages (gh-pages branch), single-widget repo.
|
|
3
2
|
//
|
|
3
|
+
// Shipped in create-grist-widget and run through its `grist-widget-deploy`
|
|
4
|
+
// bin (bin/grist-widget-deploy.mjs) by a widget's .github/workflows/deploy.yml,
|
|
5
|
+
// so a fix reaches every widget as a dependency update. The publish job runs
|
|
6
|
+
// it next to a `contents: write` token: it imports node builtins only, which
|
|
7
|
+
// tests/deploy-imports.test.mjs enforces.
|
|
8
|
+
//
|
|
4
9
|
// Adapted from the grist-widget-sdk monorepo's scripts/deploy/publish.mjs —
|
|
5
10
|
// same two channels, minus the multi-widget folder loop and manifest.json
|
|
6
11
|
// (there's only one widget here, so nothing to catalog):
|
|
@@ -15,7 +20,7 @@
|
|
|
15
20
|
// - mutable /<repo>/dev/ (+ version.json + self-reload)
|
|
16
21
|
//
|
|
17
22
|
// Design notes: dependency-free (node builtins only). Pure helpers are
|
|
18
|
-
// exported for testing; the CLI is a thin dispatch
|
|
23
|
+
// exported for testing; the CLI is a thin dispatch in bin/grist-widget-deploy.mjs.
|
|
19
24
|
//
|
|
20
25
|
// Subcommands:
|
|
21
26
|
// plan --site <dir> --repo <name> --event <push|workflow_dispatch>
|
|
@@ -226,11 +231,11 @@ export function injectSelfReload(html, sha) {
|
|
|
226
231
|
/**
|
|
227
232
|
* Scan siteDir for previously-released v<version>/showcase-meta.json files
|
|
228
233
|
* and assemble the versions.json manifest, newest first. Read by the
|
|
229
|
-
*
|
|
230
|
-
*
|
|
234
|
+
* release page grist-widget-sdk/app shows outside Grist (useReleasedVersions)
|
|
235
|
+
* to list this widget's own release history -- not the grist-widget-sdk
|
|
231
236
|
* monorepo's (found live: every scaffolded widget's version index showed
|
|
232
|
-
* the monorepo's versions instead of its own, since
|
|
233
|
-
* used to hardcode that URL). Entries whose meta has no `repo` field, or a
|
|
237
|
+
* the monorepo's versions instead of its own, since the template's version
|
|
238
|
+
* list used to hardcode that URL). Entries whose meta has no `repo` field, or a
|
|
234
239
|
* `repo` that doesn't match this repo, are skipped -- a `gh-pages` branch
|
|
235
240
|
* manually seeded from another repo's export (found live) would otherwise
|
|
236
241
|
* leak that repo's own version history into this one's index.
|
|
@@ -347,7 +352,9 @@ function git(args, cwd) {
|
|
|
347
352
|
function sleep(ms) {
|
|
348
353
|
// synchronous backoff (deploy step is not latency-sensitive)
|
|
349
354
|
const end = Date.now() + ms
|
|
350
|
-
while (Date.now() < end) {
|
|
355
|
+
while (Date.now() < end) {
|
|
356
|
+
// busy-wait: the retry loop runs synchronously
|
|
357
|
+
}
|
|
351
358
|
}
|
|
352
359
|
|
|
353
360
|
/** Regenerate versions.json (release runs only), then commit + push with rebase-retry. */
|
|
@@ -572,81 +579,3 @@ export function resetBranchesIfFirstRelease(
|
|
|
572
579
|
git(["push", "--force", "origin", "HEAD:refs/heads/dev"], repoDir)
|
|
573
580
|
return { deletedBranches, devReset: true, versionReset, identityReset, renamedFiles, installUrl }
|
|
574
581
|
}
|
|
575
|
-
|
|
576
|
-
// ----------------------------------------------------------------------------
|
|
577
|
-
// CLI
|
|
578
|
-
// ----------------------------------------------------------------------------
|
|
579
|
-
|
|
580
|
-
function parseArgs(argv) {
|
|
581
|
-
const out = { _: [] }
|
|
582
|
-
for (let i = 0; i < argv.length; i++) {
|
|
583
|
-
const a = argv[i]
|
|
584
|
-
if (a.startsWith("--")) {
|
|
585
|
-
const key = a.slice(2)
|
|
586
|
-
const next = argv[i + 1]
|
|
587
|
-
if (next === undefined || next.startsWith("--")) out[key] = true
|
|
588
|
-
else { out[key] = next; i++ }
|
|
589
|
-
} else out._.push(a)
|
|
590
|
-
}
|
|
591
|
-
return out
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function main() {
|
|
595
|
-
const [cmd, ...rest] = process.argv.slice(2)
|
|
596
|
-
const args = parseArgs(rest)
|
|
597
|
-
|
|
598
|
-
switch (cmd) {
|
|
599
|
-
case "plan": {
|
|
600
|
-
const result = plan(args.site, args.repo, args.event, args.ref, {
|
|
601
|
-
force: !!args.force,
|
|
602
|
-
})
|
|
603
|
-
process.stdout.write(JSON.stringify(result))
|
|
604
|
-
break
|
|
605
|
-
}
|
|
606
|
-
case "place": {
|
|
607
|
-
const res = placeTarget({
|
|
608
|
-
siteDir: args.site,
|
|
609
|
-
channel: args.channel,
|
|
610
|
-
version: args.version,
|
|
611
|
-
distDir: args.dist,
|
|
612
|
-
sha: args.sha,
|
|
613
|
-
ref: args.ref,
|
|
614
|
-
repo: args.repo,
|
|
615
|
-
})
|
|
616
|
-
console.log("placed:", JSON.stringify(res))
|
|
617
|
-
break
|
|
618
|
-
}
|
|
619
|
-
case "remove": {
|
|
620
|
-
const res = removeDevDir(args.site)
|
|
621
|
-
console.log("remove:", JSON.stringify(res))
|
|
622
|
-
break
|
|
623
|
-
}
|
|
624
|
-
case "finalize": {
|
|
625
|
-
const res = finalize({
|
|
626
|
-
siteDir: args.site,
|
|
627
|
-
updateVersions: !!args["update-versions"],
|
|
628
|
-
push: !!args.push,
|
|
629
|
-
commitMessage: args["commit-message"],
|
|
630
|
-
repo: args.repo,
|
|
631
|
-
})
|
|
632
|
-
console.log("finalize:", JSON.stringify(res))
|
|
633
|
-
break
|
|
634
|
-
}
|
|
635
|
-
case "reset-branches": {
|
|
636
|
-
const res = resetBranchesIfFirstRelease(
|
|
637
|
-
args["repo-dir"] || ".",
|
|
638
|
-
args.repo,
|
|
639
|
-
undefined,
|
|
640
|
-
args["owner-repo"] || process.env.GITHUB_REPOSITORY,
|
|
641
|
-
)
|
|
642
|
-
console.log("reset-branches:", JSON.stringify(res))
|
|
643
|
-
break
|
|
644
|
-
}
|
|
645
|
-
default:
|
|
646
|
-
console.error(`unknown subcommand: ${cmd || "(none)"}`)
|
|
647
|
-
process.exit(2)
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Only run the CLI when executed directly (not when imported by tests).
|
|
652
|
-
if (import.meta.url === `file://${process.argv[1]}`) main()
|
|
@@ -21,12 +21,13 @@ updates:
|
|
|
21
21
|
cooldown:
|
|
22
22
|
default-days: 7
|
|
23
23
|
groups:
|
|
24
|
-
# Minor/patch bumps arrive together as one PR. grist-widget-sdk
|
|
25
|
-
#
|
|
26
|
-
#
|
|
24
|
+
# Minor/patch bumps arrive together as one PR. grist-widget-sdk and
|
|
25
|
+
# create-grist-widget (the deploy command deploy.yml runs) are left out
|
|
26
|
+
# so their updates get a PR of their own, whose description links
|
|
27
|
+
# their release notes.
|
|
27
28
|
routine:
|
|
28
29
|
update-types: ["minor", "patch"]
|
|
29
|
-
exclude-patterns: ["grist-widget-sdk"]
|
|
30
|
+
exclude-patterns: ["grist-widget-sdk", "create-grist-widget"]
|
|
30
31
|
# @vitejs/plugin-react pins a vite major in its peer range, so their
|
|
31
32
|
# majors only build when they move together.
|
|
32
33
|
vite:
|
|
@@ -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
|
@@ -22,16 +22,17 @@ export function App() {
|
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`src/main.tsx`
|
|
26
|
-
`
|
|
27
|
-
|
|
25
|
+
`src/main.tsx` is one call, `mountGristWidget({ App, options: GRIST_OPTIONS, dev })` from
|
|
26
|
+
`grist-widget-sdk/app`: inside Grist it renders `<App />` in `GristWidgetProvider` and
|
|
27
|
+
`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 one file to edit.
|
|
28
29
|
|
|
29
30
|
## Operating contract
|
|
30
31
|
|
|
31
32
|
1. **Widgets use the SDK.** No direct `window.grist.*` calls — use `useGrist()` and the
|
|
32
33
|
other `grist-widget-sdk` hooks.
|
|
33
34
|
2. **`pnpm dev` is a bare shell, on purpose — not a seeded preview.** Opened outside Grist
|
|
34
|
-
it
|
|
35
|
+
it shows the SDK's local-dev page; the widget itself only renders once actually embedded in a
|
|
35
36
|
Grist document. This is deliberate, not a gap: an emulator can't guarantee parity with
|
|
36
37
|
real Grist, so a fake local preview would teach the wrong lesson. Trust `pnpm test`
|
|
37
38
|
(headless, real assertions) for local feedback, and the `dev`-branch loop below for real
|
|
@@ -64,10 +65,10 @@ rendering all live in `src/App.tsx` — that's the one file to edit.
|
|
|
64
65
|
| --- | --- |
|
|
65
66
|
| The widget | `src/App.tsx` |
|
|
66
67
|
| Widget test (the pattern to copy for new tests) | `src/App.test.tsx` |
|
|
67
|
-
|
|
|
68
|
+
| Smoke test (one smokeTestWidget call; CI runs it on SDK updates) | `src/App.smoke.test.tsx` |
|
|
69
|
+
| Grist wiring (mountGristWidget, inside vs. outside Grist) | `src/main.tsx` |
|
|
68
70
|
| ui primitives | `src/components/ui/` |
|
|
69
|
-
| Deploy workflow | `.github/workflows/deploy.yml` |
|
|
70
|
-
| Deploy script (version guard, channel logic) | `scripts/deploy.mjs` |
|
|
71
|
+
| Deploy workflow (runs grist-widget-deploy from the create-grist-widget devDependency: version guard, channel logic) | `.github/workflows/deploy.yml` |
|
|
71
72
|
| Pull request checks (lint, test, build) | `.github/workflows/ci.yml` |
|
|
72
73
|
| Dependency update PRs (weekly, against the dev branch) | `.github/dependabot.yml` |
|
|
73
74
|
| Full walkthrough (dev loop, deploying, embedding) | `README.md` |
|
|
@@ -95,7 +96,7 @@ Full detail, including the two live URLs (`/latest/` and `/v<version>/`): `READM
|
|
|
95
96
|
is the trusted real one
|
|
96
97
|
- Merging `dev` into `main` without bumping `package.json`'s `version` first
|
|
97
98
|
- Adding `minimumReleaseAgeExclude` entries to `pnpm-workspace.yaml` to get an install or a
|
|
98
|
-
check green, or deleting the
|
|
99
|
+
check green, or deleting the ones pnpm wrote for `grist-widget-sdk` and `create-grist-widget` on release day (see
|
|
99
100
|
`README.md`): pnpm's 24 h age check protects the widget's supply chain
|
|
100
101
|
- Merging a Dependabot pull request whose `ci.yml` checks are red, or retargeting one at
|
|
101
102
|
`main`: updates go through `dev` like any other change. Template-file updates (this
|
package/template/README.md
CHANGED
|
@@ -6,18 +6,18 @@ Base template to build a Grist widget with `grist-widget-sdk`.
|
|
|
6
6
|
**Install URL**: written here by this widget's first release (its GitHub Pages `/latest/` URL).
|
|
7
7
|
<!-- install-url:end -->
|
|
8
8
|
|
|
9
|
-
`src/main.tsx`
|
|
9
|
+
`src/main.tsx` is one call to `mountGristWidget()` from `grist-widget-sdk/app` ([reference](https://gristwidgets.com/docs/sdk/api/app)). Inside Grist it renders `App` in `GristWidgetProvider` and `GristBoundary`, under the SDK's alerts, in Grist's light/dark theme. `src/components/theme-provider.tsx` re-exports its `useTheme()`, for shadcn components that need the theme in JavaScript.
|
|
10
10
|
|
|
11
|
-
Opened outside a Grist iframe,
|
|
11
|
+
Opened outside a Grist iframe, the page is never a live preview of the widget itself, since nothing there proves anything about real Grist behavior. Local `pnpm dev` shows a local-dev page pointing at the real verification loop below (push to `dev`, open it in a real Grist document). Any built or deployed URL (root, `/latest/`, `/dev/`, `/v<version>/`, all carrying the same build, see Deployment below) shows a release page instead: which build this is, links to every other version and channel (hover one to see when it shipped), and the URL to paste into Grist's custom widget field. Both come from the SDK and follow this widget's shadcn colors; `mountGristWidget()` takes your own components instead (`LocalDevPage`, `ReleasePage`).
|
|
12
12
|
|
|
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,
|
|
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
15
|
`src/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 — same pattern as `widgets/create-email-draft`. 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/grist-types.example.ts` for typing patterns.
|
|
16
16
|
|
|
17
17
|
- **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); `
|
|
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.
|
|
19
19
|
|
|
20
|
-
Run `pnpm test`
|
|
20
|
+
Run `pnpm test`. `src/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.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
21
|
|
|
22
22
|
**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
23
|
|
|
@@ -43,11 +43,12 @@ pnpm, even under `npm create`, unless it was run with `--no-install` or
|
|
|
43
43
|
without pnpm installed. Versions are pinned with their integrity hashes, CI
|
|
44
44
|
installs exactly that, and Dependabot updates it. A `package-lock.json` isn't
|
|
45
45
|
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.
|
|
46
|
+
published less than 24 hours ago. So a widget scaffolded on the day of a
|
|
47
|
+
release also gets `minimumReleaseAgeExclude` entries in `pnpm-workspace.yaml`,
|
|
48
|
+
written by pnpm itself, for the `grist-widget-sdk` and `create-grist-widget`
|
|
49
|
+
versions it was made with, and nothing else. Keep them: without them, every
|
|
50
|
+
install (yours and CI's) rejects the lockfile until the version is a day old.
|
|
51
|
+
After that the entries are harmless.
|
|
51
52
|
|
|
52
53
|
### Live preview
|
|
53
54
|
|
|
@@ -68,8 +69,10 @@ directory, same as `scripts/smoke/create-widget.sh` does.
|
|
|
68
69
|
|
|
69
70
|
## Deployment
|
|
70
71
|
|
|
71
|
-
A bundled GitHub Actions workflow (`.github/workflows/deploy.yml
|
|
72
|
-
`
|
|
72
|
+
A bundled GitHub Actions workflow (`.github/workflows/deploy.yml`, which runs
|
|
73
|
+
the `grist-widget-deploy` command of the `create-grist-widget` devDependency)
|
|
74
|
+
publishes this widget to **your own** GitHub Pages. Fixes to the deploy logic
|
|
75
|
+
arrive as updates of that devDependency.
|
|
73
76
|
|
|
74
77
|
### The workflow: always develop on `dev`, release by merging to `main`
|
|
75
78
|
|
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.27",
|
|
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,17 @@
|
|
|
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
|
+
* Widget-specific assertions go in their own tests (see `App.test.tsx`).
|
|
10
|
+
*/
|
|
11
|
+
import { it } from "vitest"
|
|
12
|
+
import { smokeTestWidget } from "grist-widget-sdk/emulator/testing"
|
|
13
|
+
|
|
14
|
+
import App, { GRIST_OPTIONS } from "./App"
|
|
15
|
+
|
|
16
|
+
it("renders inside Grist with every declared column mapped", () =>
|
|
17
|
+
smokeTestWidget({ App, options: GRIST_OPTIONS }))
|