mirror of
https://github.com/pesde-pkg/pesde.git
synced 2025-05-04 10:33:47 +01:00
feat(website): display package readme
This commit is contained in:
parent
73618a7958
commit
a31ef0e666
10 changed files with 188 additions and 29 deletions
Binary file not shown.
|
@ -38,9 +38,18 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/nunito-sans": "^5.0.14",
|
"@fontsource-variable/nunito-sans": "^5.0.14",
|
||||||
|
"@shikijs/rehype": "^1.13.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"gunzip-maybe": "^1.4.2",
|
"gunzip-maybe": "^1.4.2",
|
||||||
"lucide-svelte": "^0.427.0",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,22 @@
|
||||||
--color-primary-hover: 255 172 42;
|
--color-primary-hover: 255 172 42;
|
||||||
--color-primary-bg: 241 157 30;
|
--color-primary-bg: 241 157 30;
|
||||||
--color-primary-fg: 10 7 4;
|
--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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
|
@ -28,15 +28,30 @@ export type TargetInfo = {
|
||||||
|
|
||||||
export type TargetKind = "roblox" | "lune" | "luau"
|
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,
|
path: string,
|
||||||
fetcher: typeof fetch,
|
fetcher: typeof fetch,
|
||||||
options?: RequestInit,
|
options?: RequestInit,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const response = await fetcher(new URL(path, PUBLIC_REGISTRY_URL), options)
|
const response = await fetchRegistry(path, fetcher, options)
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch from registry: ${response.status} ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
import type { PageServerLoad } from "./$types"
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch }) => {
|
export const load: PageServerLoad = async ({ fetch }) => {
|
||||||
const { data: packages } = await fetchRegistry<SearchResponse>("search", fetch)
|
const { data: packages } = await fetchRegistryJson<SearchResponse>("search", fetch)
|
||||||
|
|
||||||
return { packages }
|
return { packages }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import {
|
import {
|
||||||
fetchRegistry,
|
fetchRegistryJson,
|
||||||
|
RegistryHttpError,
|
||||||
type PackageVersionsResponse,
|
type PackageVersionsResponse,
|
||||||
type PackageVersionResponse,
|
type PackageVersionResponse,
|
||||||
} from "$lib/registry-api"
|
} from "$lib/registry-api"
|
||||||
|
import { error } from "@sveltejs/kit"
|
||||||
import type { LayoutServerLoad } from "./$types"
|
import type { LayoutServerLoad } from "./$types"
|
||||||
|
|
||||||
type FetchPackageOptions =
|
type FetchPackageOptions =
|
||||||
|
@ -20,23 +22,34 @@ type FetchPackageOptions =
|
||||||
const fetchPackage = async (fetcher: typeof fetch, options: FetchPackageOptions) => {
|
const fetchPackage = async (fetcher: typeof fetch, options: FetchPackageOptions) => {
|
||||||
const { scope, name } = options
|
const { scope, name } = options
|
||||||
|
|
||||||
if ("version" in options) {
|
try {
|
||||||
const { version, target } = options
|
if ("version" in options) {
|
||||||
return fetchRegistry<PackageVersionResponse>(
|
if (options.target === undefined) {
|
||||||
`packages/${encodeURIComponent(`${scope}/${name}`)}/${version}/${target}`,
|
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,
|
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 }) => {
|
export const load: LayoutServerLoad = async ({ params }) => {
|
||||||
|
|
|
@ -12,15 +12,21 @@
|
||||||
const installCommand = `pesde add ${data.pkg.name}`
|
const installCommand = `pesde add ${data.pkg.name}`
|
||||||
</script>
|
</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">
|
<div class="flex-grow pr-4">
|
||||||
<h1 class="text-3xl font-bold">
|
<h1 class="text-3xl font-bold">
|
||||||
<span class="text-heading">{scope}/</span><span class="text-light">{name}</span>
|
<span class="text-heading">{scope}/</span><span class="text-light">{name}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="mb-2 font-semibold text-primary">
|
<div class="mb-2 font-semibold text-primary">
|
||||||
v{data.pkg.version} · published {formatDistanceToNow(new Date(data.pkg.published_at), {
|
v{data.pkg.version} ·
|
||||||
addSuffix: true,
|
<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>
|
</div>
|
||||||
<p class="mb-6 max-w-prose">{data.pkg.description}</p>
|
<p class="mb-6 max-w-prose">{data.pkg.description}</p>
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import type { Config } from "tailwindcss"
|
import type { Config } from "tailwindcss"
|
||||||
import defaultTheme from "tailwindcss/defaultTheme"
|
import defaultTheme from "tailwindcss/defaultTheme"
|
||||||
|
|
||||||
|
const alpha = (color: string, alpha: number = 1) => color.replace("<alpha-value>", alpha.toString())
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||||
|
|
||||||
|
@ -45,6 +47,28 @@ export default {
|
||||||
borderColor: {
|
borderColor: {
|
||||||
DEFAULT: "rgb(var(--color-border) / <alpha-value>)",
|
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")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue