create-grist-widget 0.2.14 → 0.2.17
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
|
@@ -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:
|
package/template/README.md
CHANGED
|
@@ -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 `
|
|
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,22 @@ 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
|
+
> If you also checked **"Include all branches"** (to get `gh-pages`/Pages
|
|
95
|
+
> already set up, no manual Settings step), your first real release
|
|
96
|
+
> automatically clears out every leftover `v<version>/` it finds — a
|
|
97
|
+
> `v<version>/` only counts as a genuine prior release once it carries a
|
|
98
|
+
> `showcase-meta.json` naming *this* repo (written by this pipeline's own
|
|
99
|
+
> `place` step), so on a repo with no genuine releases yet, everything
|
|
100
|
+
> else is provably inherited noise and gets safely wiped on your first
|
|
101
|
+
> push to `main`. That cleanup only ever runs before your *first* genuine
|
|
102
|
+
> release — once one exists, it's permanently a no-op, so it's still safest
|
|
103
|
+
> to never manually re-seed `gh-pages` again after that point.
|
|
104
|
+
|
|
89
105
|
**One-time setup** (the workflow can't do this part for you):
|
|
90
106
|
|
|
91
107
|
1. **Settings → Pages** → Source: "Deploy from a branch" → branch `gh-pages`
|
package/template/package.json
CHANGED
|
@@ -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
|
|
64
|
-
*
|
|
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 =
|
|
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))
|
|
@@ -150,8 +177,31 @@ function replaceDir(dest, srcDist) {
|
|
|
150
177
|
cpSync(srcDist, dest, { recursive: true })
|
|
151
178
|
}
|
|
152
179
|
|
|
180
|
+
/**
|
|
181
|
+
* If this repo has never genuinely published a release before (no
|
|
182
|
+
* v<version>/ directory whose showcase-meta.json matches this repo), any
|
|
183
|
+
* v<version>/ directories present are provably inherited noise -- e.g. from
|
|
184
|
+
* copying this template repo including its gh-pages branch (GitHub's "Use
|
|
185
|
+
* this template" -> "Include all branches"). Safe to clear them all before
|
|
186
|
+
* placing this repo's actual first release: there is no ambiguity, since a
|
|
187
|
+
* repo with zero genuine releases can't have any of its own history to lose.
|
|
188
|
+
* Never touches dev/, latest/, or site-root files. Once a genuine release
|
|
189
|
+
* exists for this repo, buildVersionsManifest is non-empty and this is
|
|
190
|
+
* permanently a no-op.
|
|
191
|
+
*/
|
|
192
|
+
export function cleanupForeignVersionsIfFirstRelease(siteDir, repo) {
|
|
193
|
+
if (buildVersionsManifest(siteDir, repo).length > 0) return { cleaned: [] }
|
|
194
|
+
const cleaned = []
|
|
195
|
+
for (const name of existsSync(siteDir) ? readdirSync(siteDir) : []) {
|
|
196
|
+
if (!/^v\d+\.\d+\.\d+/.test(name)) continue
|
|
197
|
+
rmSync(join(siteDir, name), { recursive: true, force: true })
|
|
198
|
+
cleaned.push(name)
|
|
199
|
+
}
|
|
200
|
+
return { cleaned }
|
|
201
|
+
}
|
|
202
|
+
|
|
153
203
|
/** Place a freshly built dist into the gh-pages tree. */
|
|
154
|
-
export function placeTarget({ siteDir, channel, version, distDir, sha, ref }) {
|
|
204
|
+
export function placeTarget({ siteDir, channel, version, distDir, sha, ref, repo }) {
|
|
155
205
|
if (!existsSync(distDir)) throw new Error(`dist not found: ${distDir}`)
|
|
156
206
|
if (channel === "dev") {
|
|
157
207
|
const devDir = join(siteDir, "dev")
|
|
@@ -175,11 +225,12 @@ export function placeTarget({ siteDir, channel, version, distDir, sha, ref }) {
|
|
|
175
225
|
// exactly like latest/ reusing it: the asset URLs it references (under
|
|
176
226
|
// v<version>/assets/) already exist from the versionDir placement above,
|
|
177
227
|
// so nothing 404s.
|
|
228
|
+
cleanupForeignVersionsIfFirstRelease(siteDir, repo)
|
|
178
229
|
const versionDir = join(siteDir, `v${version}`)
|
|
179
230
|
replaceDir(versionDir, distDir)
|
|
180
231
|
writeFileSync(
|
|
181
232
|
join(versionDir, "showcase-meta.json"),
|
|
182
|
-
JSON.stringify({ version, publishedAt: new Date().toISOString(), sha }, null, 2) + "\n",
|
|
233
|
+
JSON.stringify({ version, publishedAt: new Date().toISOString(), sha, repo }, null, 2) + "\n",
|
|
183
234
|
)
|
|
184
235
|
const latestDir = join(siteDir, "latest")
|
|
185
236
|
replaceDir(latestDir, distDir)
|
|
@@ -211,9 +262,9 @@ function sleep(ms) {
|
|
|
211
262
|
}
|
|
212
263
|
|
|
213
264
|
/** Regenerate versions.json (release runs only), then commit + push with rebase-retry. */
|
|
214
|
-
export function finalize({ siteDir, updateVersions, push, commitMessage, remoteBranch = "gh-pages" }) {
|
|
265
|
+
export function finalize({ siteDir, updateVersions, push, commitMessage, remoteBranch = "gh-pages", repo }) {
|
|
215
266
|
if (updateVersions) {
|
|
216
|
-
const versions = buildVersionsManifest(siteDir)
|
|
267
|
+
const versions = buildVersionsManifest(siteDir, repo)
|
|
217
268
|
writeFileSync(join(siteDir, "versions.json"), JSON.stringify(versions, null, 2) + "\n")
|
|
218
269
|
}
|
|
219
270
|
git(["add", "-A"], siteDir)
|
|
@@ -282,6 +333,7 @@ function main() {
|
|
|
282
333
|
distDir: args.dist,
|
|
283
334
|
sha: args.sha,
|
|
284
335
|
ref: args.ref,
|
|
336
|
+
repo: args.repo,
|
|
285
337
|
})
|
|
286
338
|
console.log("placed:", JSON.stringify(res))
|
|
287
339
|
break
|
|
@@ -297,6 +349,7 @@ function main() {
|
|
|
297
349
|
updateVersions: !!args["update-versions"],
|
|
298
350
|
push: !!args.push,
|
|
299
351
|
commitMessage: args["commit-message"],
|
|
352
|
+
repo: args.repo,
|
|
300
353
|
})
|
|
301
354
|
console.log("finalize:", JSON.stringify(res))
|
|
302
355
|
break
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from "react"
|
|
2
|
-
import {
|
|
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.
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
-
<
|
|
105
|
-
<div
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
</
|
|
114
|
+
</div>
|
|
116
115
|
)
|
|
117
116
|
}
|
package/template/src/main.tsx
CHANGED
|
@@ -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
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
>
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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} />
|