create-grist-widget 0.2.12 → 0.2.14
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 +5 -0
- package/package.json +1 -1
- package/template/.github/workflows/deploy.yml +19 -6
- package/template/README.md +6 -0
- package/template/scripts/deploy.mjs +59 -7
- package/template/src/components/channel-notice.tsx +2 -2
- package/template/src/components/grist-status-chip.tsx +18 -72
- package/template/src/components/template-landing.tsx +9 -6
- package/template/src/lib/showcase-versions.ts +23 -9
package/README.md
CHANGED
|
@@ -46,6 +46,11 @@ simplest way to get from zero to a deployed widget:
|
|
|
46
46
|
folder `/ (root)`. This can only be set once `gh-pages` exists, which is
|
|
47
47
|
why it's a step *after* the first push, not before.
|
|
48
48
|
|
|
49
|
+
> ⚠️ **Not `main`.** Pointing Pages at `main` serves this repo's raw,
|
|
50
|
+
> unbuilt source instead of the built site — the symptom is a blank page
|
|
51
|
+
> with a `/src/main.tsx` 404 in the browser console, even though the
|
|
52
|
+
> workflow itself reports success.
|
|
53
|
+
|
|
49
54
|
Once that's saved, your widget is live at two URLs:
|
|
50
55
|
|
|
51
56
|
- `https://<owner>.github.io/<repo>/latest/` — mutable, always points at
|
package/package.json
CHANGED
|
@@ -13,8 +13,11 @@ name: Deploy widget to GitHub Pages
|
|
|
13
13
|
# One-time setup this workflow can't do for you: enable Pages (Settings ->
|
|
14
14
|
# Pages -> Source: Deploy from a branch -> gh-pages -> / (root); this workflow
|
|
15
15
|
# creates the gh-pages branch itself if missing, but Pages must be pointed at
|
|
16
|
-
# it once
|
|
17
|
-
#
|
|
16
|
+
# it once -- NOT main, which serves this repo's raw unbuilt source instead
|
|
17
|
+
# (symptom: a blank page with a 404 for /src/main.tsx in the browser
|
|
18
|
+
# console, even though this workflow reports success) and make sure Actions
|
|
19
|
+
# has write access (Settings -> Actions -> General -> Workflow permissions ->
|
|
20
|
+
# Read and write permissions).
|
|
18
21
|
|
|
19
22
|
on:
|
|
20
23
|
push:
|
|
@@ -56,10 +59,16 @@ jobs:
|
|
|
56
59
|
uses: actions/setup-node@v4
|
|
57
60
|
with:
|
|
58
61
|
node-version: 22
|
|
59
|
-
cache: pnpm
|
|
60
62
|
|
|
63
|
+
# Not --frozen-lockfile, and no `cache: pnpm` above: a freshly
|
|
64
|
+
# scaffolded repo never has a committed pnpm-lock.yaml (the CLI's
|
|
65
|
+
# auto-commit runs before anyone's first `pnpm install`), and
|
|
66
|
+
# --frozen-lockfile/cache: pnpm both hard-require one to exist —
|
|
67
|
+
# found live, the very first real deploy failed with "Dependencies
|
|
68
|
+
# lock file is not found". A plain install resolves fresh every run
|
|
69
|
+
# regardless of whether a lockfile was ever committed.
|
|
61
70
|
- name: Install dependencies
|
|
62
|
-
run: pnpm install
|
|
71
|
+
run: pnpm install
|
|
63
72
|
|
|
64
73
|
- name: Clone gh-pages into ./site
|
|
65
74
|
run: |
|
|
@@ -116,10 +125,14 @@ jobs:
|
|
|
116
125
|
if: steps.plan.outputs.skip == 'true'
|
|
117
126
|
run: echo "== skip — v${{ steps.plan.outputs.version }} already published"
|
|
118
127
|
|
|
119
|
-
- name: Finalize (commit + push)
|
|
128
|
+
- name: Finalize (versions.json + commit + push)
|
|
120
129
|
run: |
|
|
130
|
+
UPDATE_VERSIONS=""
|
|
131
|
+
if [ "${{ steps.plan.outputs.context }}" = "release" ]; then
|
|
132
|
+
UPDATE_VERSIONS="--update-versions"
|
|
133
|
+
fi
|
|
121
134
|
node scripts/deploy.mjs finalize \
|
|
122
|
-
--site site --push \
|
|
135
|
+
--site site $UPDATE_VERSIONS --push \
|
|
123
136
|
--commit-message "deploy: ${{ steps.plan.outputs.context }} @ ${{ github.sha }}"
|
|
124
137
|
|
|
125
138
|
cleanup:
|
package/template/README.md
CHANGED
|
@@ -92,6 +92,12 @@ automatically.
|
|
|
92
92
|
→ `/ (root)`. The workflow creates the `gh-pages` branch itself the first
|
|
93
93
|
time it runs (if it doesn't exist yet), but Pages needs to be pointed at
|
|
94
94
|
it once.
|
|
95
|
+
> ⚠️ **Not `main`.** If Pages is left on (or accidentally set to) `main`,
|
|
96
|
+
> it serves this repo's raw, unbuilt source — including a script tag
|
|
97
|
+
> pointing at `/src/main.tsx` — instead of the built site. The symptom is
|
|
98
|
+
> a blank/black page with a 404 for `/src/main.tsx` in the browser
|
|
99
|
+
> console, even though the workflow itself reports success (it pushed the
|
|
100
|
+
> right build to `gh-pages`; Pages is just reading from the wrong place).
|
|
95
101
|
2. **Settings → Actions → General → Workflow permissions** → "Read and write
|
|
96
102
|
permissions". New repos sometimes default the workflow's token to
|
|
97
103
|
read-only, which would fail the push to `gh-pages` with a 403.
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// release (push to main / workflow_dispatch)
|
|
9
9
|
// - immutable /<repo>/v<version>/ (built once, never overwritten)
|
|
10
10
|
// - mutable /<repo>/latest/ (alias to newest release)
|
|
11
|
+
// - /<repo>/versions.json (released-version index, newest first
|
|
12
|
+
// -- read by the showcase hub/notice)
|
|
11
13
|
//
|
|
12
14
|
// dev (push to dev)
|
|
13
15
|
// - mutable /<repo>/dev/ (+ version.json + self-reload)
|
|
@@ -30,10 +32,19 @@
|
|
|
30
32
|
// Removes dev/ from the gh-pages tree (retire the dev URL when the `dev`
|
|
31
33
|
// branch is deleted). Never touches a release or latest/.
|
|
32
34
|
//
|
|
33
|
-
// finalize --site <dir> [--push] [--commit-message <msg>]
|
|
34
|
-
//
|
|
35
|
+
// finalize --site <dir> [--update-versions] [--push] [--commit-message <msg>]
|
|
36
|
+
// Regenerates versions.json (release runs only) and commits + pushes the
|
|
37
|
+
// gh-pages tree with a rebase-and-retry loop.
|
|
35
38
|
|
|
36
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
existsSync,
|
|
41
|
+
readFileSync,
|
|
42
|
+
readdirSync,
|
|
43
|
+
writeFileSync,
|
|
44
|
+
rmSync,
|
|
45
|
+
mkdirSync,
|
|
46
|
+
cpSync,
|
|
47
|
+
} from "node:fs"
|
|
37
48
|
import { join, dirname } from "node:path"
|
|
38
49
|
import { execFileSync } from "node:child_process"
|
|
39
50
|
|
|
@@ -107,6 +118,28 @@ export function injectSelfReload(html, sha) {
|
|
|
107
118
|
return out + snippet
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Scan siteDir for previously-released v<version>/showcase-meta.json files
|
|
123
|
+
* and assemble the versions.json manifest, newest first. Read by the
|
|
124
|
+
* showcase hub (TemplateLanding) and per-channel notice (ChannelNotice) to
|
|
125
|
+
* show this widget's own release history -- not the grist-widget-sdk
|
|
126
|
+
* monorepo's (found live: every scaffolded widget's version index showed
|
|
127
|
+
* the monorepo's versions instead of its own, since showcase-versions.ts
|
|
128
|
+
* used to hardcode that URL).
|
|
129
|
+
*/
|
|
130
|
+
export function buildVersionsManifest(siteDir) {
|
|
131
|
+
const entries = []
|
|
132
|
+
for (const name of existsSync(siteDir) ? readdirSync(siteDir) : []) {
|
|
133
|
+
if (!/^v\d+\.\d+\.\d+/.test(name)) continue
|
|
134
|
+
const metaPath = join(siteDir, name, "showcase-meta.json")
|
|
135
|
+
if (!existsSync(metaPath)) continue
|
|
136
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf8"))
|
|
137
|
+
entries.push({ version: meta.version, publishedAt: meta.publishedAt })
|
|
138
|
+
}
|
|
139
|
+
entries.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt))
|
|
140
|
+
return entries
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
// ----------------------------------------------------------------------------
|
|
111
144
|
// Filesystem / git effects
|
|
112
145
|
// ----------------------------------------------------------------------------
|
|
@@ -133,12 +166,26 @@ export function placeTarget({ siteDir, channel, version, distDir, sha, ref }) {
|
|
|
133
166
|
}
|
|
134
167
|
return { placed: "dev" }
|
|
135
168
|
}
|
|
136
|
-
// release
|
|
169
|
+
// release: immutable v<version>/, mutable latest/, and the same build
|
|
170
|
+
// placed directly at the site root too. main.tsx renders the showcase hub
|
|
171
|
+
// (TemplateLanding) at any URL with no recognized channel suffix — the
|
|
172
|
+
// bare root included — so without this, a real scaffolded repo's own
|
|
173
|
+
// https://owner.github.io/repo/ has nothing deployed there at all and
|
|
174
|
+
// 404s instead of ever reaching that hub. Reusing the release dist here is
|
|
175
|
+
// exactly like latest/ reusing it: the asset URLs it references (under
|
|
176
|
+
// v<version>/assets/) already exist from the versionDir placement above,
|
|
177
|
+
// so nothing 404s.
|
|
137
178
|
const versionDir = join(siteDir, `v${version}`)
|
|
138
179
|
replaceDir(versionDir, distDir)
|
|
180
|
+
writeFileSync(
|
|
181
|
+
join(versionDir, "showcase-meta.json"),
|
|
182
|
+
JSON.stringify({ version, publishedAt: new Date().toISOString(), sha }, null, 2) + "\n",
|
|
183
|
+
)
|
|
139
184
|
const latestDir = join(siteDir, "latest")
|
|
140
185
|
replaceDir(latestDir, distDir)
|
|
141
|
-
|
|
186
|
+
mkdirSync(siteDir, { recursive: true })
|
|
187
|
+
cpSync(distDir, siteDir, { recursive: true })
|
|
188
|
+
return { placed: `v${version}`, latest: "latest", root: true }
|
|
142
189
|
}
|
|
143
190
|
|
|
144
191
|
/**
|
|
@@ -163,8 +210,12 @@ function sleep(ms) {
|
|
|
163
210
|
while (Date.now() < end) {}
|
|
164
211
|
}
|
|
165
212
|
|
|
166
|
-
/**
|
|
167
|
-
export function finalize({ siteDir, push, commitMessage, remoteBranch = "gh-pages" }) {
|
|
213
|
+
/** Regenerate versions.json (release runs only), then commit + push with rebase-retry. */
|
|
214
|
+
export function finalize({ siteDir, updateVersions, push, commitMessage, remoteBranch = "gh-pages" }) {
|
|
215
|
+
if (updateVersions) {
|
|
216
|
+
const versions = buildVersionsManifest(siteDir)
|
|
217
|
+
writeFileSync(join(siteDir, "versions.json"), JSON.stringify(versions, null, 2) + "\n")
|
|
218
|
+
}
|
|
168
219
|
git(["add", "-A"], siteDir)
|
|
169
220
|
const status = git(["status", "--porcelain"], siteDir)
|
|
170
221
|
if (!status) {
|
|
@@ -243,6 +294,7 @@ function main() {
|
|
|
243
294
|
case "finalize": {
|
|
244
295
|
const res = finalize({
|
|
245
296
|
siteDir: args.site,
|
|
297
|
+
updateVersions: !!args["update-versions"],
|
|
246
298
|
push: !!args.push,
|
|
247
299
|
commitMessage: args["commit-message"],
|
|
248
300
|
})
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "@/components/ui/card"
|
|
11
11
|
import { Input } from "@/components/ui/input"
|
|
12
12
|
import { channelLabel, type Channel } from "@/lib/showcase-routing"
|
|
13
|
-
import {
|
|
13
|
+
import { devUrl, useVersions, versionUrl } from "@/lib/showcase-versions"
|
|
14
14
|
import { cn } from "@/lib/utils"
|
|
15
15
|
|
|
16
16
|
// Shown instead of the widget tree on a specific channel build
|
|
@@ -44,7 +44,7 @@ function VersionChips({ current }: { current: Channel }) {
|
|
|
44
44
|
latest
|
|
45
45
|
</a>
|
|
46
46
|
)}
|
|
47
|
-
<a href={
|
|
47
|
+
<a href={devUrl()} className={chipClass(current.kind === "dev")}>
|
|
48
48
|
dev
|
|
49
49
|
</a>
|
|
50
50
|
{versions?.map((v) => (
|
|
@@ -49,7 +49,7 @@ function StatusDot({ pulsing, className }: { pulsing: boolean; className: string
|
|
|
49
49
|
)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** Pure render of a display status
|
|
52
|
+
/** Pure render of a display status. */
|
|
53
53
|
function ChipVisual({ display, secondsLeft }: { display: HandshakeDisplayStatus; secondsLeft: number }) {
|
|
54
54
|
switch (display.kind) {
|
|
55
55
|
case "connecting":
|
|
@@ -91,81 +91,27 @@ function RealChipContents() {
|
|
|
91
91
|
return <ChipVisual display={display} secondsLeft={secondsLeft} />
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// A fixed, looping sequence for the showcase hub -- there's no real Grist to
|
|
95
|
-
// connect to on that page (it's shown precisely because a real widget isn't
|
|
96
|
-
// embedded), so this is a cosmetic demo of the same visual states a real
|
|
97
|
-
// widget's chip goes through, not a real handshake attempt.
|
|
98
|
-
const DEMO_SEQUENCE: readonly { display: HandshakeDisplayStatus; ms: number }[] = [
|
|
99
|
-
{ display: { kind: "connecting" }, ms: 1500 },
|
|
100
|
-
{ display: { kind: "retrying", attempts: 1 }, ms: 3000 },
|
|
101
|
-
{ display: { kind: "connected" }, ms: 3500 },
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
function DemoChipContents() {
|
|
105
|
-
const [step, setStep] = useState(0)
|
|
106
|
-
const [countdownState, setCountdownState] = useState({ step, secondsLeft: 3 })
|
|
107
|
-
const current = DEMO_SEQUENCE[step]
|
|
108
|
-
|
|
109
|
-
// Reset the mini countdown the moment the demo cycle enters "retrying" --
|
|
110
|
-
// an "adjust state during render" pattern, not a setState-in-effect.
|
|
111
|
-
if (current.display.kind === "retrying" && countdownState.step !== step) {
|
|
112
|
-
setCountdownState({ step, secondsLeft: 3 })
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
const advance = setTimeout(() => {
|
|
117
|
-
setStep((s) => (s + 1) % DEMO_SEQUENCE.length)
|
|
118
|
-
}, current.ms)
|
|
119
|
-
return () => clearTimeout(advance)
|
|
120
|
-
}, [step, current.ms])
|
|
121
|
-
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
if (current.display.kind !== "retrying") return
|
|
124
|
-
const id = setInterval(() => {
|
|
125
|
-
setCountdownState((s) => ({ ...s, secondsLeft: s.secondsLeft > 0 ? s.secondsLeft - 1 : 0 }))
|
|
126
|
-
}, 1000)
|
|
127
|
-
return () => clearInterval(id)
|
|
128
|
-
}, [step, current.display.kind])
|
|
129
|
-
|
|
130
|
-
return <ChipVisual display={current.display} secondsLeft={countdownState.secondsLeft} />
|
|
131
|
-
}
|
|
132
|
-
|
|
133
94
|
/**
|
|
134
|
-
* Small pill showing Grist handshake status
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* it never drives the real widget's connection, just reflects it.
|
|
141
|
-
* - `demo`: a cosmetic, looping cycle through the same visual states, used
|
|
142
|
-
* on the showcase hub page where no real Grist connection is ever
|
|
143
|
-
* attempted -- a reference to the tech behind the template, not a status
|
|
144
|
-
* readout.
|
|
95
|
+
* 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.
|
|
145
101
|
*/
|
|
146
|
-
export function GristStatusChip({
|
|
147
|
-
demo = false,
|
|
148
|
-
className,
|
|
149
|
-
}: {
|
|
150
|
-
demo?: boolean
|
|
151
|
-
className?: string
|
|
152
|
-
}) {
|
|
153
|
-
const pill = (
|
|
154
|
-
<div
|
|
155
|
-
className={cn(
|
|
156
|
-
"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",
|
|
157
|
-
className,
|
|
158
|
-
)}
|
|
159
|
-
>
|
|
160
|
-
{demo ? <DemoChipContents /> : <RealChipContents />}
|
|
161
|
-
</div>
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
if (demo) return pill
|
|
165
|
-
|
|
102
|
+
export function GristStatusChip({ className }: { className?: string }) {
|
|
166
103
|
return (
|
|
167
104
|
<GristHandshakeProvider>
|
|
168
|
-
<div className="pointer-events-none fixed top-2 right-2 z-50">
|
|
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>
|
|
114
|
+
</div>
|
|
169
115
|
</GristHandshakeProvider>
|
|
170
116
|
)
|
|
171
117
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { useState } from "react"
|
|
2
2
|
|
|
3
|
-
import { GristStatusChip } from "@/components/grist-status-chip"
|
|
4
3
|
import { Button } from "@/components/ui/button"
|
|
5
4
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
6
|
-
import {
|
|
5
|
+
import { devUrl, formatDate, useVersions, versionUrl } from "@/lib/showcase-versions"
|
|
7
6
|
|
|
8
7
|
// The showcase "hub" page: rendered whenever this deploy's URL has no
|
|
9
8
|
// recognized channel suffix (see src/lib/showcase-routing.ts) -- this repo's
|
|
@@ -70,9 +69,6 @@ export function TemplateLanding() {
|
|
|
70
69
|
return (
|
|
71
70
|
<div className="mx-auto max-w-2xl px-6 py-16">
|
|
72
71
|
<header className="mb-10 text-center">
|
|
73
|
-
<div className="mb-4 flex justify-center">
|
|
74
|
-
<GristStatusChip demo />
|
|
75
|
-
</div>
|
|
76
72
|
<p className="mb-2 text-sm font-medium text-muted-foreground">
|
|
77
73
|
grist-widget-template
|
|
78
74
|
</p>
|
|
@@ -157,6 +153,13 @@ git push origin dev`}</code>
|
|
|
157
153
|
enable Pages: Settings → Pages → Source → "Deploy from a
|
|
158
154
|
branch" → branch <code className="text-foreground">gh-pages</code>{" "}
|
|
159
155
|
→ <code className="text-foreground">/ (root)</code>.
|
|
156
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
157
|
+
Not <code className="text-foreground">main</code> — that
|
|
158
|
+
serves this repo's raw, unbuilt source instead of the built
|
|
159
|
+
site (a blank page with a{" "}
|
|
160
|
+
<code className="text-foreground">/src/main.tsx</code> 404
|
|
161
|
+
in the console).
|
|
162
|
+
</p>
|
|
160
163
|
<p className="mt-2 text-xs text-muted-foreground">
|
|
161
164
|
If that first run fails with a permissions error pushing to{" "}
|
|
162
165
|
<code className="text-foreground">gh-pages</code>, Settings
|
|
@@ -175,7 +178,7 @@ git push origin dev`}</code>
|
|
|
175
178
|
Released template versions
|
|
176
179
|
</h2>
|
|
177
180
|
<a
|
|
178
|
-
href={
|
|
181
|
+
href={devUrl()}
|
|
179
182
|
className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
|
180
183
|
>
|
|
181
184
|
Live dev preview →
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
import { useEffect, useState } from "react"
|
|
2
2
|
|
|
3
|
+
import { parseShowcasePath } from "@/lib/showcase-routing"
|
|
4
|
+
|
|
3
5
|
// Shared by the showcase hub (TemplateLanding) and the per-channel notice
|
|
4
|
-
// (ChannelNotice) -- both need the same released-version list
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
// (ChannelNotice) -- both need the same released-version list. URLs are
|
|
7
|
+
// root-relative to wherever THIS deploy actually lives (derived from
|
|
8
|
+
// window.location.pathname via parseShowcasePath's hubPath), not hardcoded
|
|
9
|
+
// to the grist-widget-sdk monorepo's own showcase: a real scaffolded
|
|
10
|
+
// widget has its own versions.json and its own v<version>/ + dev/ dirs,
|
|
11
|
+
// and previously showed the monorepo's version history and links instead
|
|
12
|
+
// of its own (found live on a real scaffold's landing page).
|
|
13
|
+
function hubPath(): string {
|
|
14
|
+
const path = parseShowcasePath(window.location.pathname).hubPath
|
|
15
|
+
return path.endsWith("/") ? path : `${path}/`
|
|
16
|
+
}
|
|
11
17
|
|
|
12
18
|
export type ReleasedVersion = {
|
|
13
19
|
version: string
|
|
14
20
|
publishedAt: string
|
|
15
21
|
}
|
|
16
22
|
|
|
23
|
+
export function versionsUrl(): string {
|
|
24
|
+
return `${hubPath()}versions.json`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function devUrl(): string {
|
|
28
|
+
return `${hubPath()}dev/`
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
export function versionUrl(version: string): string {
|
|
18
|
-
return
|
|
32
|
+
return `${hubPath()}v${version}/`
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
export function useVersions() {
|
|
@@ -24,7 +38,7 @@ export function useVersions() {
|
|
|
24
38
|
|
|
25
39
|
useEffect(() => {
|
|
26
40
|
let cancelled = false
|
|
27
|
-
fetch(
|
|
41
|
+
fetch(versionsUrl(), { cache: "no-store" })
|
|
28
42
|
.then((res) => (res.ok ? res.json() : Promise.reject(res)))
|
|
29
43
|
.then((data: ReleasedVersion[]) => {
|
|
30
44
|
if (!cancelled) setVersions(data)
|