feat(website): display package readme

This commit is contained in:
LukaDev 2024-08-14 22:32:01 +02:00
parent 73618a7958
commit a31ef0e666
10 changed files with 188 additions and 29 deletions

Binary file not shown.

View file

@ -38,9 +38,18 @@
"type": "module",
"dependencies": {
"@fontsource-variable/nunito-sans": "^5.0.14",
"@shikijs/rehype": "^1.13.0",
"date-fns": "^3.6.0",
"gunzip-maybe": "^1.4.2",
"lucide-svelte": "^0.427.0",
"tar-stream": "^3.1.7"
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"shiki": "^1.13.0",
"tar-stream": "^3.1.7",
"unified": "^11.0.5"
}
}

View file

@ -20,6 +20,22 @@
--color-primary-hover: 255 172 42;
--color-primary-bg: 241 157 30;
--color-primary-fg: 10 7 4;
--shiki-foreground: rgb(var(--color-heading));
--shiki-background: rgb(var(--color-card));
--shiki-token-constant: color-mix(in srgb, rgb(120 140 230), rgb(var(--color-light)) 50%);
--shiki-token-string: rgb(var(--color-heading));
--shiki-token-comment: rgb(var(--color-body));
--shiki-token-keyword: color-mix(in srgb, rgb(var(--color-primary)), rgb(var(--color-light)) 50%);
--shiki-token-parameter: rgb(var(--color-heading));
--shiki-token-function: rgb(var(--color-primary));
--shiki-token-string-expression: color-mix(
in srgb,
rgb(120 230 140),
rgb(var(--color-light)) 50%
);
--shiki-token-punctuation: rgb(var(--color-heading));
--shiki-token-link: rgb(var(--color-primary));
}
@media (prefers-color-scheme: dark) {

View file

@ -28,15 +28,30 @@ export type TargetInfo = {
export type TargetKind = "roblox" | "lune" | "luau"
export async function fetchRegistry<T>(
export class RegistryHttpError extends Error {
name = "RegistryError"
constructor(
message: string,
public response: Response,
) {
super(message)
}
}
export async function fetchRegistryJson<T>(
path: string,
fetcher: typeof fetch,
options?: RequestInit,
): Promise<T> {
const response = await fetcher(new URL(path, PUBLIC_REGISTRY_URL), options)
if (!response.ok) {
throw new Error(`Failed to fetch from registry: ${response.status} ${response.statusText}`)
}
const response = await fetchRegistry(path, fetcher, options)
return response.json()
}
export async function fetchRegistry(path: string, fetcher: typeof fetch, options?: RequestInit) {
const response = await fetcher(new URL(path, PUBLIC_REGISTRY_URL), options)
if (!response.ok) {
throw new RegistryHttpError(`Failed to fetch ${response.url}: ${response.statusText}`, response)
}
return response
}

View file

@ -1,8 +1,8 @@
import { fetchRegistry, type SearchResponse } from "$lib/registry-api"
import { fetchRegistryJson, type SearchResponse } from "$lib/registry-api"
import type { PageServerLoad } from "./$types"
export const load: PageServerLoad = async ({ fetch }) => {
const { data: packages } = await fetchRegistry<SearchResponse>("search", fetch)
const { data: packages } = await fetchRegistryJson<SearchResponse>("search", fetch)
return { packages }
}

View file

@ -1,8 +1,10 @@
import {
fetchRegistry,
fetchRegistryJson,
RegistryHttpError,
type PackageVersionsResponse,
type PackageVersionResponse,
} from "$lib/registry-api"
import { error } from "@sveltejs/kit"
import type { LayoutServerLoad } from "./$types"
type FetchPackageOptions =
@ -20,23 +22,34 @@ type FetchPackageOptions =
const fetchPackage = async (fetcher: typeof fetch, options: FetchPackageOptions) => {
const { scope, name } = options
if ("version" in options) {
const { version, target } = options
return fetchRegistry<PackageVersionResponse>(
`packages/${encodeURIComponent(`${scope}/${name}`)}/${version}/${target}`,
try {
if ("version" in options) {
if (options.target === undefined) {
error(404, "Not Found")
}
const { version, target } = options
return fetchRegistryJson<PackageVersionResponse>(
`packages/${encodeURIComponent(`${scope}/${name}`)}/${version}/${target}`,
fetcher,
)
}
const versions = await fetchRegistryJson<PackageVersionsResponse>(
`packages/${encodeURIComponent(`${scope}/${name}`)}`,
fetcher,
)
const latestVersion = versions.at(-1)
if (latestVersion === undefined) throw new Error("package has no versions *blows up*")
return latestVersion
} catch (e) {
if (e instanceof RegistryHttpError && e.response.status === 404) {
error(404, "This package does not exist.")
}
throw e
}
const versions = await fetchRegistry<PackageVersionsResponse>(
`packages/${encodeURIComponent(`${scope}/${name}`)}`,
fetcher,
)
const latestVersion = versions.at(-1)
if (latestVersion === undefined) throw new Error("package has no versions *blows up*")
return latestVersion
}
export const load: LayoutServerLoad = async ({ params }) => {

View file

@ -12,15 +12,21 @@
const installCommand = `pesde add ${data.pkg.name}`
</script>
<div class="mx-auto flex max-w-screen-xl px-4 py-16">
<div class="mx-auto flex max-w-screen-lg px-4 py-16">
<div class="flex-grow pr-4">
<h1 class="text-3xl font-bold">
<span class="text-heading">{scope}/</span><span class="text-light">{name}</span>
</h1>
<div class="mb-2 font-semibold text-primary">
v{data.pkg.version} · published {formatDistanceToNow(new Date(data.pkg.published_at), {
addSuffix: true,
})}
v{data.pkg.version} ·
<time
datetime={data.pkg.published_at}
title={new Date(data.pkg.published_at).toLocaleString()}
>
published {formatDistanceToNow(new Date(data.pkg.published_at), {
addSuffix: true,
})}
</time>
</div>
<p class="mb-6 max-w-prose">{data.pkg.description}</p>

View file

@ -0,0 +1,69 @@
import { fetchRegistry, RegistryHttpError } from "$lib/registry-api"
import { unified } from "unified"
import type { PageServerLoad } from "./$types"
import remarkParse from "remark-parse"
import remarkRehype from "remark-rehype"
import rehypeSanitize from "rehype-sanitize"
import rehypeStringify from "rehype-stringify"
import rehypeRaw from "rehype-raw"
import remarkGfm from "remark-gfm"
import rehypeShiki from "@shikijs/rehype"
import { createCssVariablesTheme } from "shiki"
const fetchReadme = async (
fetcher: typeof fetch,
name: string,
version: string,
target: string,
) => {
try {
const res = await fetchRegistry(
`packages/${encodeURIComponent(name)}/${version}/${target}`,
fetcher,
{
headers: {
Accept: "text/plain",
},
},
)
return res.text()
} catch (e) {
if (e instanceof RegistryHttpError && e.response.status === 404) {
return "*No README provided*"
}
throw e
}
}
export const load: PageServerLoad = async ({ parent }) => {
const { pkg } = await parent()
const { name, version, targets } = pkg
const readmeText = await fetchReadme(fetch, name, version, targets[0].kind)
const file = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSanitize)
.use(rehypeShiki, {
theme: createCssVariablesTheme({
name: "css-variables",
variablePrefix: "--shiki-",
variableDefaults: {},
fontStyle: true,
}),
defaultLanguage: "text",
})
.use(rehypeStringify)
.process(readmeText)
const readmeHtml = file.value
return {
readmeHtml,
pkg,
}
}

View file

@ -1 +1,8 @@
README
<script>
const { data } = $props()
</script>
<div class="prose max-w-none py-8">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html data.readmeHtml}
</div>

View file

@ -1,6 +1,8 @@
import type { Config } from "tailwindcss"
import defaultTheme from "tailwindcss/defaultTheme"
const alpha = (color: string, alpha: number = 1) => color.replace("<alpha-value>", alpha.toString())
export default {
content: ["./src/**/*.{html,js,svelte,ts}"],
@ -45,6 +47,28 @@ export default {
borderColor: {
DEFAULT: "rgb(var(--color-border) / <alpha-value>)",
},
typography: ({ theme }) => ({
DEFAULT: {
css: {
"--tw-prose-body": alpha(theme("colors.body")),
"--tw-prose-headings": alpha(theme("colors.heading")),
"--tw-prose-lead": alpha(theme("colors.heading")),
"--tw-prose-links": alpha(theme("colors.primary").DEFAULT),
"--tw-prose-bold": alpha(theme("colors.body")),
"--tw-prose-counters": alpha(theme("colors.body")),
"--tw-prose-bullets": alpha(theme("colors.border")),
"--tw-prose-hr": alpha(theme("colors.border")),
"--tw-prose-quotes": alpha(theme("colors.body")),
"--tw-prose-quote-borders": alpha(theme("colors.border")),
"--tw-prose-captions": alpha(theme("colors.body")),
"--tw-prose-code": alpha(theme("colors.body")),
"--tw-prose-pre-code": alpha(theme("colors.body")),
"--tw-prose-pre-bg": alpha(theme("colors.card").DEFAULT),
"--tw-prose-th-borders": alpha(theme("colors.border")),
"--tw-prose-td-borders": alpha(theme("colors.border")),
},
},
}),
},
},