create-grist-widget 0.2.18 → 0.2.20
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 +15 -0
- package/package.json +1 -1
- package/template/.github/workflows/deploy.yml +13 -0
- package/template/README.md +23 -13
- package/template/scripts/deploy.mjs +62 -2
- package/template/src/components/channel-notice.tsx +3 -0
- package/template/src/components/scaffold-footer.tsx +17 -0
- package/template/src/components/template-landing.tsx +3 -0
- package/template/src/vite-env.d.ts +6 -0
- package/template/vite.config.ts +9 -0
|
@@ -50,6 +50,21 @@ for (const [file, subs] of edits) {
|
|
|
50
50
|
writeFileSync(p, content)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// Record which create-grist-widget release produced this scaffold -- purely
|
|
54
|
+
// informational (shown in a small landing-page footer), never read by the
|
|
55
|
+
// deploy pipeline's own version/release logic. Every scaffold gets this,
|
|
56
|
+
// including the ones the template-canary workflow generates for its `dev`
|
|
57
|
+
// and `canary/latest` branches -- so it survives even if that content is
|
|
58
|
+
// later promoted into a reference repo's `main` and copied again via
|
|
59
|
+
// GitHub's "Use this template".
|
|
60
|
+
const cliPkg = JSON.parse(
|
|
61
|
+
readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"),
|
|
62
|
+
)
|
|
63
|
+
const pkgJsonPath = path.join(target, "package.json")
|
|
64
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"))
|
|
65
|
+
pkgJson.createGristWidgetVersion = cliPkg.version
|
|
66
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n")
|
|
67
|
+
|
|
53
68
|
// Initialize git on `main` with the scaffold as its first commit, and a
|
|
54
69
|
// `dev` branch ready to push for live-preview later. Doing this here (not
|
|
55
70
|
// leaving it to whoever/whatever runs next) is what actually prevents the
|
package/package.json
CHANGED
|
@@ -101,6 +101,7 @@ jobs:
|
|
|
101
101
|
fs.appendFileSync(out, `skip=${p.skip ? "true" : "false"}\n`);
|
|
102
102
|
fs.appendFileSync(out, `version=${p.version || ""}\n`);
|
|
103
103
|
fs.appendFileSync(out, `base=${p.base}\n`);
|
|
104
|
+
fs.appendFileSync(out, `firstRelease=${p.firstRelease ? "true" : "false"}\n`);
|
|
104
105
|
' "$PLAN"
|
|
105
106
|
|
|
106
107
|
- name: Build
|
|
@@ -135,6 +136,18 @@ jobs:
|
|
|
135
136
|
--site site --repo "$REPO_NAME" $UPDATE_VERSIONS --push \
|
|
136
137
|
--commit-message "deploy: ${{ steps.plan.outputs.context }} @ ${{ github.sha }}"
|
|
137
138
|
|
|
139
|
+
# On this repo's first genuine release only: prune every branch except
|
|
140
|
+
# main/dev/gh-pages, and reset dev to main's current tip. Establishes
|
|
141
|
+
# the schema every widget should start from (main == dev, gh-pages is
|
|
142
|
+
# CI-managed only) regardless of how the repo was created -- a CLI
|
|
143
|
+
# scaffold already satisfies this trivially; a repo copied via GitHub's
|
|
144
|
+
# "Use this template" often doesn't (a stray inherited branch, or dev
|
|
145
|
+
# tracking the source template's own unrelated preview history).
|
|
146
|
+
# Placed after Finalize so it never runs if the release itself failed.
|
|
147
|
+
- name: Reset branches on first genuine release
|
|
148
|
+
if: steps.plan.outputs.context == 'release' && steps.plan.outputs.firstRelease == 'true'
|
|
149
|
+
run: node scripts/deploy.mjs reset-branches
|
|
150
|
+
|
|
138
151
|
cleanup:
|
|
139
152
|
# When the `dev` branch is deleted, remove /dev/ so the dev URL retires
|
|
140
153
|
# (404) instead of serving a stale build forever.
|
package/template/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Base template to build a Grist widget with `grist-widget-sdk`.
|
|
|
4
4
|
|
|
5
5
|
`src/main.tsx` wraps the app with `GristWidgetProvider`, `GristBoundary`, and `GristSdkAlerts`. The latter maps `getGristSdkAlertDescriptors()` from the SDK to shadcn `Alert` (`src/components/grist-sdk-alerts.tsx` + `src/components/ui/alert.tsx`); keep them in sync with the playground when you change alert styling.
|
|
6
6
|
|
|
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.
|
|
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. Both render `ScaffoldFooter` (`src/components/scaffold-footer.tsx`) at the bottom — a small, purely informational note ("Scaffolded from `create-grist-widget@X.Y.Z`") built from `package.json`'s `createGristWidgetVersion` field, which the CLI stamps on every real scaffold (`vite.config.ts` injects it as `__CREATE_GRIST_WIDGET_VERSION__`). Renders nothing if that field is absent, e.g. this pristine template source itself.
|
|
8
8
|
|
|
9
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
|
|
|
@@ -89,18 +89,28 @@ automatically.
|
|
|
89
89
|
> ⚠️ **Used GitHub's "Use this template" button instead of `npm create
|
|
90
90
|
> grist-widget`?** That path copies this repo's default branch verbatim —
|
|
91
91
|
> it doesn't run any of the CLI's own setup (renaming `package.json`,
|
|
92
|
-
> titles, etc.), and
|
|
93
|
-
>
|
|
94
|
-
>
|
|
95
|
-
>
|
|
96
|
-
>
|
|
97
|
-
>
|
|
98
|
-
>
|
|
99
|
-
> `
|
|
100
|
-
>
|
|
101
|
-
>
|
|
102
|
-
>
|
|
103
|
-
>
|
|
92
|
+
> titles, etc.), and whatever version happens to be checked into the
|
|
93
|
+
> source repo's `package.json` (which can be well past `0.0.1` — see below)
|
|
94
|
+
> comes along with it. You don't need to reset it by hand: your first real
|
|
95
|
+
> release always resolves to `v0.0.1` regardless of what `package.json`
|
|
96
|
+
> says, and that value stays untouched in the file — only the deploy
|
|
97
|
+
> pipeline's own idea of "which version is this" is overridden, once, for
|
|
98
|
+
> that first release. If you also checked **"Include all branches"** (to
|
|
99
|
+
> get `gh-pages`/Pages already set up, no manual Settings step), your first
|
|
100
|
+
> real release also automatically clears out every leftover `v<version>/`
|
|
101
|
+
> it finds — a `v<version>/` only counts as a genuine prior release once it
|
|
102
|
+
> carries a `showcase-meta.json` naming *this* repo (written by this
|
|
103
|
+
> pipeline's own `place` step), so on a repo with no genuine releases yet,
|
|
104
|
+
> everything else is provably inherited noise and gets safely wiped on your
|
|
105
|
+
> first push to `main`. Also on that same first release, every branch
|
|
106
|
+
> except `main`, `dev`, and `gh-pages` gets pruned, and `dev` gets
|
|
107
|
+
> force-reset to match `main`'s tip — a fresh widget always starts from
|
|
108
|
+
> `main == dev`, regardless of whether it came from the CLI (already true)
|
|
109
|
+
> or a template copy (may have inherited a stray branch, or a `dev` full of
|
|
110
|
+
> the source template's own unrelated preview history). All three of these
|
|
111
|
+
> only ever apply before your *first* genuine release — once one exists,
|
|
112
|
+
> they're permanently a no-op, so it's still safest to never manually
|
|
113
|
+
> re-seed `gh-pages` again after that point.
|
|
104
114
|
|
|
105
115
|
**One-time setup** (the workflow can't do this part for you):
|
|
106
116
|
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
// finalize --site <dir> [--update-versions] [--push] [--commit-message <msg>]
|
|
36
36
|
// Regenerates versions.json (release runs only) and commits + pushes the
|
|
37
37
|
// gh-pages tree with a rebase-and-retry loop.
|
|
38
|
+
//
|
|
39
|
+
// reset-branches [--repo-dir <dir>]
|
|
40
|
+
// On a repo's first genuine release only (caller gates on plan()'s
|
|
41
|
+
// firstRelease): prunes every branch except main/dev/gh-pages and
|
|
42
|
+
// force-resets dev to main's current tip.
|
|
38
43
|
|
|
39
44
|
import {
|
|
40
45
|
existsSync,
|
|
@@ -80,6 +85,16 @@ function isGenuineRelease(versionDir, repo) {
|
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Whether this repo has ever genuinely published a release (a v<version>/
|
|
90
|
+
* dir whose showcase-meta.json names this repo). Shared by `plan()` (forces
|
|
91
|
+
* the first release to 0.0.1) and `cleanupForeignVersionsIfFirstRelease()`
|
|
92
|
+
* (clears inherited v<version>/ noise) -- both need the exact same signal.
|
|
93
|
+
*/
|
|
94
|
+
function hasGenuineRelease(siteDir, repo) {
|
|
95
|
+
return buildVersionsManifest(siteDir, repo).length > 0
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/**
|
|
84
99
|
* Build the release/dev plan from this repo's own package.json + the current
|
|
85
100
|
* gh-pages tree. Release is skipped when its immutable v<version> dir was
|
|
@@ -92,7 +107,15 @@ export function plan(siteDir, repo, event, ref, { force = false } = {}) {
|
|
|
92
107
|
return { context: "dev", base: basePathFor(repo, "dev") }
|
|
93
108
|
}
|
|
94
109
|
const pkg = JSON.parse(readFileSync("package.json", "utf8"))
|
|
95
|
-
|
|
110
|
+
// A repo's first genuine release always starts at 0.0.1, regardless of
|
|
111
|
+
// whatever package.json's version field says -- found live: a repo copied
|
|
112
|
+
// via GitHub's "Use this template" inherited "0.2.18" from the source
|
|
113
|
+
// template repo's own main branch (itself a canary-stamped scaffold that
|
|
114
|
+
// had been promoted there), instead of starting fresh. Once a repo has a
|
|
115
|
+
// genuine release, this no longer applies -- its own version bumps are
|
|
116
|
+
// its own responsibility from then on, same as `cleanupForeignVersionsIfFirstRelease`.
|
|
117
|
+
const firstRelease = !hasGenuineRelease(siteDir, repo)
|
|
118
|
+
const version = firstRelease ? "0.0.1" : pkg.version
|
|
96
119
|
if (!version) throw new Error("package.json has no version")
|
|
97
120
|
const versionDir = join(siteDir, `v${version}`)
|
|
98
121
|
const exists = isGenuineRelease(versionDir, repo)
|
|
@@ -102,6 +125,10 @@ export function plan(siteDir, repo, event, ref, { force = false } = {}) {
|
|
|
102
125
|
base: basePathFor(repo, "release", version),
|
|
103
126
|
skip: exists && !force,
|
|
104
127
|
reason: exists ? (force ? "force-rebuild" : "already-published") : "new-version",
|
|
128
|
+
// Exposed so the workflow can gate a one-time repo cleanup (prune stray
|
|
129
|
+
// branches, reset dev to main) on exactly this signal -- see
|
|
130
|
+
// `resetBranchesIfFirstRelease`.
|
|
131
|
+
firstRelease,
|
|
105
132
|
}
|
|
106
133
|
}
|
|
107
134
|
|
|
@@ -190,7 +217,7 @@ function replaceDir(dest, srcDist) {
|
|
|
190
217
|
* permanently a no-op.
|
|
191
218
|
*/
|
|
192
219
|
export function cleanupForeignVersionsIfFirstRelease(siteDir, repo) {
|
|
193
|
-
if (
|
|
220
|
+
if (hasGenuineRelease(siteDir, repo)) return { cleaned: [] }
|
|
194
221
|
const cleaned = []
|
|
195
222
|
for (const name of existsSync(siteDir) ? readdirSync(siteDir) : []) {
|
|
196
223
|
if (!/^v\d+\.\d+\.\d+/.test(name)) continue
|
|
@@ -297,6 +324,34 @@ export function finalize({ siteDir, updateVersions, push, commitMessage, remoteB
|
|
|
297
324
|
throw lastErr
|
|
298
325
|
}
|
|
299
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Establish the schema every widget repo should start from -- `main` and
|
|
329
|
+
* `dev` identical, `gh-pages` managed by CI only -- by pruning every other
|
|
330
|
+
* branch and force-resetting `dev` to `main`'s current tip (the commit
|
|
331
|
+
* already checked out at `repoDir`). Runs once, gated by the caller on
|
|
332
|
+
* `plan()`'s `firstRelease` flag, so it applies equally to a CLI scaffold
|
|
333
|
+
* (already satisfies this trivially -- nothing to prune, dev already equals
|
|
334
|
+
* main) and a repo copied via GitHub's "Use this template" (may have a
|
|
335
|
+
* stray inherited branch, or `dev` tracking the source template's own
|
|
336
|
+
* unrelated preview history). Operates on `repoDir`, the repo's own
|
|
337
|
+
* checkout -- a different directory than the gh-pages `siteDir` every other
|
|
338
|
+
* function here works with.
|
|
339
|
+
*/
|
|
340
|
+
export function resetBranchesIfFirstRelease(repoDir, keep = ["main", "dev", "gh-pages"]) {
|
|
341
|
+
const raw = git(["ls-remote", "--heads", "origin"], repoDir)
|
|
342
|
+
const branches = raw
|
|
343
|
+
? raw.split("\n").map((line) => line.split("refs/heads/")[1]).filter(Boolean)
|
|
344
|
+
: []
|
|
345
|
+
const deletedBranches = []
|
|
346
|
+
for (const b of branches) {
|
|
347
|
+
if (keep.includes(b)) continue
|
|
348
|
+
git(["push", "origin", "--delete", b], repoDir)
|
|
349
|
+
deletedBranches.push(b)
|
|
350
|
+
}
|
|
351
|
+
git(["push", "--force", "origin", "HEAD:refs/heads/dev"], repoDir)
|
|
352
|
+
return { deletedBranches, devReset: true }
|
|
353
|
+
}
|
|
354
|
+
|
|
300
355
|
// ----------------------------------------------------------------------------
|
|
301
356
|
// CLI
|
|
302
357
|
// ----------------------------------------------------------------------------
|
|
@@ -354,6 +409,11 @@ function main() {
|
|
|
354
409
|
console.log("finalize:", JSON.stringify(res))
|
|
355
410
|
break
|
|
356
411
|
}
|
|
412
|
+
case "reset-branches": {
|
|
413
|
+
const res = resetBranchesIfFirstRelease(args["repo-dir"] || ".")
|
|
414
|
+
console.log("reset-branches:", JSON.stringify(res))
|
|
415
|
+
break
|
|
416
|
+
}
|
|
357
417
|
default:
|
|
358
418
|
console.error(`unknown subcommand: ${cmd || "(none)"}`)
|
|
359
419
|
process.exit(2)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react"
|
|
2
2
|
|
|
3
|
+
import { ScaffoldFooter } from "@/components/scaffold-footer"
|
|
3
4
|
import { Button } from "@/components/ui/button"
|
|
4
5
|
import {
|
|
5
6
|
Card,
|
|
@@ -117,6 +118,8 @@ export function ChannelNotice({
|
|
|
117
118
|
>
|
|
118
119
|
← Back to the template home
|
|
119
120
|
</a>
|
|
121
|
+
|
|
122
|
+
<ScaffoldFooter />
|
|
120
123
|
</div>
|
|
121
124
|
)
|
|
122
125
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Shown on the showcase hub and channel pages, never inside Grist (there's
|
|
2
|
+
// no room, and it's not relevant to using the widget). Purely informational:
|
|
3
|
+
// which create-grist-widget release this repo's package.json was scaffolded
|
|
4
|
+
// from -- distinct from the repo's own `version`, which the deploy pipeline
|
|
5
|
+
// always starts fresh at 0.0.1 regardless of this value (see
|
|
6
|
+
// scripts/deploy.mjs's `plan()`). Renders nothing for the pristine template
|
|
7
|
+
// source itself, or any copy predating this field.
|
|
8
|
+
export function ScaffoldFooter() {
|
|
9
|
+
if (!__CREATE_GRIST_WIDGET_VERSION__) return null
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<p className="mt-10 text-center text-xs text-muted-foreground">
|
|
13
|
+
Scaffolded from{" "}
|
|
14
|
+
<code className="text-foreground">create-grist-widget@{__CREATE_GRIST_WIDGET_VERSION__}</code>
|
|
15
|
+
</p>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react"
|
|
2
2
|
|
|
3
|
+
import { ScaffoldFooter } from "@/components/scaffold-footer"
|
|
3
4
|
import { Button } from "@/components/ui/button"
|
|
4
5
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
5
6
|
import { devUrl, formatDate, useVersions, versionUrl } from "@/lib/showcase-versions"
|
|
@@ -301,6 +302,8 @@ git push origin dev`}</code>
|
|
|
301
302
|
</ul>
|
|
302
303
|
)}
|
|
303
304
|
</section>
|
|
305
|
+
|
|
306
|
+
<ScaffoldFooter />
|
|
304
307
|
</div>
|
|
305
308
|
)
|
|
306
309
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
// Injected by vite.config.ts's `define` from package.json's
|
|
4
|
+
// createGristWidgetVersion field -- null for the pristine template source
|
|
5
|
+
// itself (that field only exists on scaffolded copies).
|
|
6
|
+
declare const __CREATE_GRIST_WIDGET_VERSION__: string | null
|
package/template/vite.config.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { readFileSync } from "fs"
|
|
1
2
|
import path from "path"
|
|
2
3
|
import tailwindcss from "@tailwindcss/vite"
|
|
3
4
|
import react from "@vitejs/plugin-react"
|
|
4
5
|
import { defineConfig } from "vite"
|
|
5
6
|
|
|
7
|
+
// Read here (Node context) rather than importing package.json into src/ --
|
|
8
|
+
// keeps the whole file out of the bundle, exposing only the one field the
|
|
9
|
+
// landing page footer needs.
|
|
10
|
+
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "package.json"), "utf8"))
|
|
11
|
+
|
|
6
12
|
// https://vite.dev/config/
|
|
7
13
|
export default defineConfig({
|
|
8
14
|
plugins: [react(), tailwindcss()],
|
|
@@ -11,4 +17,7 @@ export default defineConfig({
|
|
|
11
17
|
"@": path.resolve(__dirname, "./src"),
|
|
12
18
|
},
|
|
13
19
|
},
|
|
20
|
+
define: {
|
|
21
|
+
__CREATE_GRIST_WIDGET_VERSION__: JSON.stringify(pkg.createGristWidgetVersion ?? null),
|
|
22
|
+
},
|
|
14
23
|
})
|