create-grist-widget 0.2.14 → 0.2.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-grist-widget",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Scaffold a new Grist custom widget from the grist-widget-sdk Vite template",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -132,7 +132,7 @@ jobs:
132
132
  UPDATE_VERSIONS="--update-versions"
133
133
  fi
134
134
  node scripts/deploy.mjs finalize \
135
- --site site $UPDATE_VERSIONS --push \
135
+ --site site --repo "$REPO_NAME" $UPDATE_VERSIONS --push \
136
136
  --commit-message "deploy: ${{ steps.plan.outputs.context }} @ ${{ github.sha }}"
137
137
 
138
138
  cleanup:
@@ -6,7 +6,7 @@ Base template to build a Grist widget with `grist-widget-sdk`.
6
6
 
7
7
  Opened outside a Grist iframe, `main.tsx` picks between two components purely by URL shape (`src/lib/showcase-routing.ts`, no router needed): a bare path with no recognized channel suffix renders `TemplateLanding` (its own hero, onboarding, and released-version index); `/latest/`, `/dev/`, or `/v<version>/` renders `ChannelNotice` instead — a distinct hero explaining which build this is, chips to jump to any other version/channel (`src/lib/showcase-versions.ts`), and a copy-this-URL helper for pasting into Grist's custom widget field.
8
8
 
9
- When actually embedded, `GristStatusChip` (`src/components/grist-status-chip.tsx`) shows a small pill with live handshake status — connecting, retrying with a countdown, connected, or unavailable — using `useGristHandshakeContext()` from `grist-widget-sdk/advanced` in its own `<GristHandshakeProvider>`. Safe to mount alongside `GristWidgetProvider`: both share the page's `ensureGristReady()` singleton (see `apps/docs/api/handshake.md`), so this is purely observational and never duplicates the real handshake.
9
+ When actually embedded, `GristStatusChip` (`src/components/grist-status-chip.tsx`) shows a small pill with live handshake status — connecting, retrying with a countdown, connected, or unavailable — using `useAmbientGristHandshake()` from `grist-widget-sdk/advanced`. It must be mounted *inside* `GristWidgetProvider` (see `src/main.tsx`): it only observes that provider's own handshake manager and never mounts a second one, so it's purely observational and can never duplicate or race the real handshake (see `apps/docs/api/handshake.md`).
10
10
 
11
11
  `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`. See `src/grist-types.example.ts` for typing patterns.
12
12
 
@@ -86,6 +86,19 @@ of changes — it's the permanent working/preview branch for this widget, not
86
86
  a one-off feature branch to delete and recreate. Deleting it retires `/dev/`
87
87
  automatically.
88
88
 
89
+ > ⚠️ **Used GitHub's "Use this template" button instead of `npm create
90
+ > grist-widget`?** That path copies this repo's default branch verbatim —
91
+ > it doesn't run any of the CLI's own setup (renaming `package.json`,
92
+ > titles, etc.), and it won't reset `package.json`'s `version` back to a
93
+ > fresh `0.0.1` for you. Do both by hand before your first real release.
94
+ > Also **never manually seed/copy `gh-pages` content** from another repo
95
+ > (e.g. to get Pages showing something immediately) — each `v<version>/`
96
+ > is only recognized as a genuine prior release if it carries a
97
+ > `showcase-meta.json` naming *this* repo (written by this pipeline's own
98
+ > `place` step); anything else is treated as unpublished and safely
99
+ > rebuilt over, but it's simpler to just let the workflow create
100
+ > `gh-pages` itself (it does, as an orphan branch, on its first real run).
101
+
89
102
  **One-time setup** (the workflow can't do this part for you):
90
103
 
91
104
  1. **Settings → Pages** → Source: "Deploy from a branch" → branch `gh-pages`
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@fontsource-variable/geist": "^5.2.8",
16
- "grist-widget-sdk": "^0.2.1",
16
+ "grist-widget-sdk": "^0.2.15",
17
17
  "@tailwindcss/vite": "^4.2.1",
18
18
  "class-variance-authority": "^0.7.1",
19
19
  "clsx": "^2.1.1",
@@ -58,10 +58,33 @@ export function basePathFor(repo, channel, version) {
58
58
  return `/${repo}/${leaf}/`
59
59
  }
60
60
 
61
+ /**
62
+ * Was `versionDir` actually published by *this* repo's own pipeline? Checks
63
+ * for a `showcase-meta.json` whose `repo` field matches -- not just that the
64
+ * directory exists. A `gh-pages` branch manually seeded from another repo's
65
+ * export (found live: a freshly-created widget repo's `gh-pages` already
66
+ * contained `v<version>/` directories copied from the template repo it came
67
+ * from, some with no meta file at all, one with a foreign `showcase-meta.json`
68
+ * whose `sha` didn't belong to this repo's own history) would otherwise look
69
+ * indistinguishable from a real prior release and get silently treated as
70
+ * "already published" forever, permanently squatting on that version path.
71
+ */
72
+ function isGenuineRelease(versionDir, repo) {
73
+ const metaPath = join(versionDir, "showcase-meta.json")
74
+ if (!existsSync(metaPath)) return false
75
+ try {
76
+ const meta = JSON.parse(readFileSync(metaPath, "utf8"))
77
+ return meta.repo === repo
78
+ } catch {
79
+ return false
80
+ }
81
+ }
82
+
61
83
  /**
62
84
  * Build the release/dev plan from this repo's own package.json + the current
63
- * gh-pages tree. Release is skipped when its immutable v<version> dir already
64
- * exists (unless force), so re-pushing main without a version bump is a no-op.
85
+ * gh-pages tree. Release is skipped when its immutable v<version> dir was
86
+ * genuinely published by this repo before (unless force), so re-pushing main
87
+ * without a version bump is a no-op.
65
88
  */
66
89
  export function plan(siteDir, repo, event, ref, { force = false } = {}) {
67
90
  const isRelease = event === "workflow_dispatch" || ref === "refs/heads/main" || ref === "main"
@@ -72,7 +95,7 @@ export function plan(siteDir, repo, event, ref, { force = false } = {}) {
72
95
  const version = pkg.version
73
96
  if (!version) throw new Error("package.json has no version")
74
97
  const versionDir = join(siteDir, `v${version}`)
75
- const exists = existsSync(versionDir)
98
+ const exists = isGenuineRelease(versionDir, repo)
76
99
  return {
77
100
  context: "release",
78
101
  version,
@@ -125,15 +148,19 @@ export function injectSelfReload(html, sha) {
125
148
  * show this widget's own release history -- not the grist-widget-sdk
126
149
  * monorepo's (found live: every scaffolded widget's version index showed
127
150
  * the monorepo's versions instead of its own, since showcase-versions.ts
128
- * used to hardcode that URL).
151
+ * used to hardcode that URL). Entries whose meta has no `repo` field, or a
152
+ * `repo` that doesn't match this repo, are skipped -- a `gh-pages` branch
153
+ * manually seeded from another repo's export (found live) would otherwise
154
+ * leak that repo's own version history into this one's index.
129
155
  */
130
- export function buildVersionsManifest(siteDir) {
156
+ export function buildVersionsManifest(siteDir, repo) {
131
157
  const entries = []
132
158
  for (const name of existsSync(siteDir) ? readdirSync(siteDir) : []) {
133
159
  if (!/^v\d+\.\d+\.\d+/.test(name)) continue
134
160
  const metaPath = join(siteDir, name, "showcase-meta.json")
135
161
  if (!existsSync(metaPath)) continue
136
162
  const meta = JSON.parse(readFileSync(metaPath, "utf8"))
163
+ if (meta.repo !== repo) continue
137
164
  entries.push({ version: meta.version, publishedAt: meta.publishedAt })
138
165
  }
139
166
  entries.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt))
@@ -151,7 +178,7 @@ function replaceDir(dest, srcDist) {
151
178
  }
152
179
 
153
180
  /** Place a freshly built dist into the gh-pages tree. */
154
- export function placeTarget({ siteDir, channel, version, distDir, sha, ref }) {
181
+ export function placeTarget({ siteDir, channel, version, distDir, sha, ref, repo }) {
155
182
  if (!existsSync(distDir)) throw new Error(`dist not found: ${distDir}`)
156
183
  if (channel === "dev") {
157
184
  const devDir = join(siteDir, "dev")
@@ -179,7 +206,7 @@ export function placeTarget({ siteDir, channel, version, distDir, sha, ref }) {
179
206
  replaceDir(versionDir, distDir)
180
207
  writeFileSync(
181
208
  join(versionDir, "showcase-meta.json"),
182
- JSON.stringify({ version, publishedAt: new Date().toISOString(), sha }, null, 2) + "\n",
209
+ JSON.stringify({ version, publishedAt: new Date().toISOString(), sha, repo }, null, 2) + "\n",
183
210
  )
184
211
  const latestDir = join(siteDir, "latest")
185
212
  replaceDir(latestDir, distDir)
@@ -211,9 +238,9 @@ function sleep(ms) {
211
238
  }
212
239
 
213
240
  /** Regenerate versions.json (release runs only), then commit + push with rebase-retry. */
214
- export function finalize({ siteDir, updateVersions, push, commitMessage, remoteBranch = "gh-pages" }) {
241
+ export function finalize({ siteDir, updateVersions, push, commitMessage, remoteBranch = "gh-pages", repo }) {
215
242
  if (updateVersions) {
216
- const versions = buildVersionsManifest(siteDir)
243
+ const versions = buildVersionsManifest(siteDir, repo)
217
244
  writeFileSync(join(siteDir, "versions.json"), JSON.stringify(versions, null, 2) + "\n")
218
245
  }
219
246
  git(["add", "-A"], siteDir)
@@ -282,6 +309,7 @@ function main() {
282
309
  distDir: args.dist,
283
310
  sha: args.sha,
284
311
  ref: args.ref,
312
+ repo: args.repo,
285
313
  })
286
314
  console.log("placed:", JSON.stringify(res))
287
315
  break
@@ -297,6 +325,7 @@ function main() {
297
325
  updateVersions: !!args["update-versions"],
298
326
  push: !!args.push,
299
327
  commitMessage: args["commit-message"],
328
+ repo: args.repo,
300
329
  })
301
330
  console.log("finalize:", JSON.stringify(res))
302
331
  break
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from "react"
2
- import { GristHandshakeProvider, useGristHandshakeContext } from "grist-widget-sdk/advanced"
2
+ import { useAmbientGristHandshake, type GristWidgetSnapshot } from "grist-widget-sdk/advanced"
3
3
 
4
4
  import { deriveDisplayStatus, type HandshakeDisplayStatus } from "@/lib/grist-handshake-status"
5
5
  import { cn } from "@/lib/utils"
@@ -83,8 +83,7 @@ function ChipVisual({ display, secondsLeft }: { display: HandshakeDisplayStatus;
83
83
  }
84
84
  }
85
85
 
86
- function RealChipContents() {
87
- const { snapshot } = useGristHandshakeContext()
86
+ function RealChipContents({ snapshot }: { snapshot: GristWidgetSnapshot }) {
88
87
  const display = deriveDisplayStatus(snapshot.lifecycle)
89
88
  const retryAttempts = display.kind === "retrying" ? display.attempts : 0
90
89
  const secondsLeft = useRetryCountdown(retryAttempts, display.kind === "retrying")
@@ -93,25 +92,25 @@ function RealChipContents() {
93
92
 
94
93
  /**
95
94
  * Small pill showing live Grist handshake status while actually embedded in
96
- * Grist. Mounts its own `<GristHandshakeProvider>` alongside
97
- * `<GristWidgetProvider>` -- documented (grist-widget-sdk/advanced) as safe
98
- * to coexist, since both share the page's `ensureGristReady()` singleton.
99
- * Purely observational: it never drives the real widget's connection, just
100
- * reflects it.
95
+ * Grist. Must be mounted *inside* `<GristWidgetProvider>` -- it only observes
96
+ * that provider's own handshake manager (`useAmbientGristHandshake()`,
97
+ * `grist-widget-sdk/advanced`) and never mounts its own, so it can't ever run
98
+ * a second, competing `grist.ready()` call. Renders nothing outside a
99
+ * provider.
101
100
  */
102
101
  export function GristStatusChip({ className }: { className?: string }) {
102
+ const handshake = useAmbientGristHandshake()
103
+ if (!handshake) return null
103
104
  return (
104
- <GristHandshakeProvider>
105
- <div className="pointer-events-none fixed top-2 right-2 z-50">
106
- <div
107
- className={cn(
108
- "flex items-center gap-1.5 rounded-full border bg-background/90 px-2.5 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur",
109
- className,
110
- )}
111
- >
112
- <RealChipContents />
113
- </div>
105
+ <div className="pointer-events-none fixed top-2 right-2 z-50">
106
+ <div
107
+ className={cn(
108
+ "flex items-center gap-1.5 rounded-full border bg-background/90 px-2.5 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur",
109
+ className,
110
+ )}
111
+ >
112
+ <RealChipContents snapshot={handshake.snapshot} />
114
113
  </div>
115
- </GristHandshakeProvider>
114
+ </div>
116
115
  )
117
116
  }
@@ -35,20 +35,18 @@ createRoot(document.getElementById("root")!).render(
35
35
  <TemplateLanding />
36
36
  </div>
37
37
  ) : isEmbedded ? (
38
- <>
38
+ <GristWidgetProvider options={GRIST_OPTIONS}>
39
39
  <GristStatusChip />
40
- <GristWidgetProvider options={GRIST_OPTIONS}>
41
- <GristBoundary
42
- gate={GRIST_OPTIONS.columns?.length ? "canRender" : "ready"}
43
- >
44
- <div className="min-h-full w-full bg-background text-foreground">
45
- <GristSdkAlerts>
46
- <App />
47
- </GristSdkAlerts>
48
- </div>
49
- </GristBoundary>
50
- </GristWidgetProvider>
51
- </>
40
+ <GristBoundary
41
+ gate={GRIST_OPTIONS.columns?.length ? "canRender" : "ready"}
42
+ >
43
+ <div className="min-h-full w-full bg-background text-foreground">
44
+ <GristSdkAlerts>
45
+ <App />
46
+ </GristSdkAlerts>
47
+ </div>
48
+ </GristBoundary>
49
+ </GristWidgetProvider>
52
50
  ) : (
53
51
  <div className="min-h-full w-full bg-background text-foreground">
54
52
  <ChannelNotice channel={channel} hubPath={hubPath} />