mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-05-17 14:55:47 +02:00
Add README fetching and decoding functionality in RepoPage. Enhanced error handling and improved rendering of README content with support for relative URLs. Updated API response handling to include download URL.
This commit is contained in:
+22
-4
@@ -5,6 +5,15 @@ import { sortTagEntries, type TagEntry } from "./lib/tagSemver"
|
||||
|
||||
const TAG_TTL_MS = 120_000
|
||||
|
||||
function decodeBase64Utf8(b64: string): string {
|
||||
const normalized = b64.replace(/\s/g, "")
|
||||
if (normalized === "") return ""
|
||||
const binary = atob(normalized)
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
||||
return new TextDecoder("utf-8").decode(bytes)
|
||||
}
|
||||
|
||||
const tagEntryValidator = v.object({
|
||||
name: v.string(),
|
||||
sha: v.string(),
|
||||
@@ -115,15 +124,24 @@ export const fetchReadme = action({
|
||||
handler: async (_ctx, args) => {
|
||||
const token = process.env.GITHUB_TOKEN
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/vnd.github.raw+json",
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
}
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
const enc = encodeURIComponent(args.ref)
|
||||
const res = await fetch(`https://api.github.com/repos/${args.owner}/${args.repo}/readme?ref=${enc}`, { headers })
|
||||
if (res.status === 404) return { markdown: "" as string, missing: true as const }
|
||||
if (res.status === 404) return { markdown: "" as string, readmeDownloadUrl: null as null, missing: true as const }
|
||||
if (!res.ok) throw new Error(`readme: ${res.status} ${await res.text()}`)
|
||||
const markdown = await res.text()
|
||||
return { markdown, missing: false as const }
|
||||
const json = (await res.json()) as {
|
||||
content?: string
|
||||
download_url?: string | null
|
||||
}
|
||||
const b64 = json.content ?? ""
|
||||
const markdown = decodeBase64Utf8(b64)
|
||||
return {
|
||||
markdown,
|
||||
readmeDownloadUrl: (json.download_url ?? null) as string | null,
|
||||
missing: false as const,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/** Resolve README-relative URLs the same way GitHub does, using the README file's raw download URL as base. */
|
||||
export function resolveReadmeRelativeUrl(
|
||||
hrefOrSrc: string | undefined,
|
||||
readmeDownloadUrl: string | null
|
||||
): string | undefined {
|
||||
if (!hrefOrSrc || !readmeDownloadUrl) return hrefOrSrc
|
||||
const trimmed = hrefOrSrc.trim()
|
||||
if (!trimmed) return hrefOrSrc
|
||||
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return hrefOrSrc
|
||||
if (trimmed.startsWith("//")) return hrefOrSrc
|
||||
if (trimmed.startsWith("#")) return hrefOrSrc
|
||||
try {
|
||||
const download = new URL(readmeDownloadUrl)
|
||||
const slash = download.pathname.lastIndexOf("/")
|
||||
const dirPath = slash >= 0 ? download.pathname.slice(0, slash + 1) : "/"
|
||||
const base = `${download.origin}${dirPath}`
|
||||
return new URL(trimmed, base).href
|
||||
} catch {
|
||||
return hrefOrSrc
|
||||
}
|
||||
}
|
||||
+55
-8
@@ -3,7 +3,15 @@ import { api } from "@/convex/_generated/api"
|
||||
import { sortTagNames } from "@/convex/lib/tagSemver"
|
||||
import { useAction, useMutation, useQuery } from "convex/react"
|
||||
import { Github, Link2, RefreshCw } from "lucide-react"
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
||||
import {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type AnchorHTMLAttributes,
|
||||
type ImgHTMLAttributes,
|
||||
} from "react"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import { Link, useNavigate, useParams } from "react-router-dom"
|
||||
import rehypeRaw from "rehype-raw"
|
||||
@@ -15,6 +23,7 @@ import EspFlasher from "../components/EspFlasher"
|
||||
import { normalizeBuildKey } from "../lib/buildKey"
|
||||
import { buildFailurePresentation } from "../lib/formatBuildErrorSummary"
|
||||
import { homepageHref, homepageLabel } from "../lib/githubHomepage"
|
||||
import { resolveReadmeRelativeUrl } from "../lib/readmeAssetUrl"
|
||||
import { buildTreeSplatPath, parseTreeSplat } from "../lib/repoTreeUrl"
|
||||
|
||||
const MESH_FORGE_ACTIONS_REPO = "MeshEnvy/mesh-forge"
|
||||
@@ -92,22 +101,58 @@ export default function RepoPage() {
|
||||
)
|
||||
|
||||
const [readmeMd, setReadmeMd] = useState<string | null>(null)
|
||||
const [readmeDownloadUrl, setReadmeDownloadUrl] = useState<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (!effectiveRef) return
|
||||
if (!effectiveRef) {
|
||||
setReadmeMd(null)
|
||||
setReadmeDownloadUrl(null)
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
setReadmeMd(null)
|
||||
setReadmeDownloadUrl(null)
|
||||
void fetchReadme({ owner, repo, ref: effectiveRef })
|
||||
.then(r => {
|
||||
if (!cancelled) setReadmeMd(r.markdown)
|
||||
if (!cancelled) {
|
||||
setReadmeMd(r.markdown)
|
||||
setReadmeDownloadUrl(r.readmeDownloadUrl ?? null)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setReadmeMd("*(README could not be loaded.)*")
|
||||
if (!cancelled) {
|
||||
setReadmeMd("*(README could not be loaded.)*")
|
||||
setReadmeDownloadUrl(null)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [owner, repo, effectiveRef, fetchReadme])
|
||||
|
||||
const readmeMarkdownComponents = useMemo(
|
||||
() => ({
|
||||
img: ({ src, ...props }: ImgHTMLAttributes<HTMLImageElement>) => (
|
||||
<img {...props} src={resolveReadmeRelativeUrl(src, readmeDownloadUrl) ?? src} />
|
||||
),
|
||||
a: ({ href, children, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const resolved = resolveReadmeRelativeUrl(href, readmeDownloadUrl) ?? href
|
||||
if (resolved?.startsWith("/")) {
|
||||
return (
|
||||
<Link to={resolved} {...props}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<a href={resolved} target="_blank" rel="noreferrer" {...props}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
}),
|
||||
[readmeDownloadUrl]
|
||||
)
|
||||
|
||||
const envNames = scan?.scanStatus === "complete" ? (scan.envNames ?? []) : []
|
||||
const resolvedTargetEnv =
|
||||
hasRef && targetFromUrl && envNames.length > 0 && envNames.includes(targetFromUrl) ? targetFromUrl : ""
|
||||
@@ -298,9 +343,7 @@ export default function RepoPage() {
|
||||
|
||||
const flashButtonLabel = buildInProgress ? "Building…" : "Flash"
|
||||
|
||||
const showCiCard =
|
||||
build &&
|
||||
(build.status !== "failed" || witnessedCiFailure)
|
||||
const showCiCard = build && (build.status !== "failed" || witnessedCiFailure)
|
||||
|
||||
const targetPlaceholder = !hasRef
|
||||
? "--target--"
|
||||
@@ -550,7 +593,11 @@ export default function RepoPage() {
|
||||
{readmeMd === null ? (
|
||||
<p className="text-slate-500 not-prose">Loading…</p>
|
||||
) : (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw, rehypeSanitize]}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
||||
components={readmeMarkdownComponents}
|
||||
>
|
||||
{readmeMd || "*No README.*"}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./types.ts","./vite.config.ts","./components/DiscordButton.tsx","./components/Footer.tsx","./components/Navbar.tsx","./components/RedditButton.tsx","./components/ui/button.tsx","./components/ui/checkbox.tsx","./components/ui/input.tsx","./components/ui/popover.tsx","./components/ui/sonner.tsx","./components/ui/switch.tsx","./constants/targets.ts","./constants/versions.ts","./convex/actions.ts","./convex/admin.ts","./convex/auth.config.ts","./convex/auth.ts","./convex/deviceReports.ts","./convex/helpers.ts","./convex/http.ts","./convex/repoBuildDownloads.ts","./convex/repoBuilds.ts","./convex/repoScans.ts","./convex/repoTags.ts","./convex/schema.ts","./convex/_generated/api.d.ts","./convex/_generated/dataModel.d.ts","./convex/_generated/server.d.ts","./convex/lib/platformioScan.ts","./convex/lib/r2.ts","./convex/lib/tagSemver.ts","./lib/utils.ts","./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/ComboboxField.tsx","./src/components/EspFlasher.tsx","./src/components/MarkdownDoc.tsx","./src/lib/buildKey.ts","./src/lib/espFlashLayout.ts","./src/lib/espFlashRun.ts","./src/lib/formatBuildErrorSummary.ts","./src/lib/githubHomepage.ts","./src/lib/parseGithubUrl.ts","./src/lib/readmeAssetUrl.ts","./src/lib/repoTreeUrl.ts","./src/lib/untarGz.ts","./src/pages/AdminPage.tsx","./src/pages/HomePage.tsx","./src/pages/LegalLicensePage.tsx","./src/pages/LegalPrivacyPage.tsx","./src/pages/LegalTermsPage.tsx","./src/pages/NotFoundPage.tsx","./src/pages/RepoPage.tsx","./vendor/meshscript/examples/types.ts","./vendor/meshscript/examples/ubbs.ts","./vendor/meshscript/src/api.ts","./vendor/meshtastic-firmware/meshtestic/src/discover.ts","./vendor/meshtastic-firmware/meshtestic/src/node.ts","./vendor/meshtastic-firmware/meshtestic/src/setup.ts","./vendor/meshtastic-firmware/meshtestic/src/util.ts","./vendor/meshtastic-firmware/meshtestic/test/config.spec.ts","./vendor/meshtastic-firmware/protobufs/packages/ts/mod.ts"],"errors":true,"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user