diff --git a/website/bun.lockb b/website/bun.lockb index 545cfce..94c679c 100755 Binary files a/website/bun.lockb and b/website/bun.lockb differ diff --git a/website/package.json b/website/package.json index 66526ee..e728d78 100644 --- a/website/package.json +++ b/website/package.json @@ -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" } } diff --git a/website/src/app.css b/website/src/app.css index 8803aeb..660b89e 100644 --- a/website/src/app.css +++ b/website/src/app.css @@ -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) { diff --git a/website/src/lib/registry-api.ts b/website/src/lib/registry-api.ts index e5302c7..d16d842 100644 --- a/website/src/lib/registry-api.ts +++ b/website/src/lib/registry-api.ts @@ -28,15 +28,30 @@ export type TargetInfo = { export type TargetKind = "roblox" | "lune" | "luau" -export async function fetchRegistry( +export class RegistryHttpError extends Error { + name = "RegistryError" + constructor( + message: string, + public response: Response, + ) { + super(message) + } +} + +export async function fetchRegistryJson( path: string, fetcher: typeof fetch, options?: RequestInit, ): Promise { - 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 +} diff --git a/website/src/routes/+page.server.ts b/website/src/routes/+page.server.ts index a419b94..8d1ae78 100644 --- a/website/src/routes/+page.server.ts +++ b/website/src/routes/+page.server.ts @@ -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("search", fetch) + const { data: packages } = await fetchRegistryJson("search", fetch) return { packages } } diff --git a/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+layout.server.ts b/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+layout.server.ts index 42ca03a..7951665 100644 --- a/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+layout.server.ts +++ b/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+layout.server.ts @@ -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( - `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( + `packages/${encodeURIComponent(`${scope}/${name}`)}/${version}/${target}`, + fetcher, + ) + } + + const versions = await fetchRegistryJson( + `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( - `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 }) => { diff --git a/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+layout.svelte b/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+layout.svelte index f5c703e..0c973e3 100644 --- a/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+layout.svelte +++ b/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+layout.svelte @@ -12,15 +12,21 @@ const installCommand = `pesde add ${data.pkg.name}` -
+

{scope}/{name}

- v{data.pkg.version} · published {formatDistanceToNow(new Date(data.pkg.published_at), { - addSuffix: true, - })} + v{data.pkg.version} · +

{data.pkg.description}

diff --git a/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+page.server.ts b/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+page.server.ts new file mode 100644 index 0000000..8623bf2 --- /dev/null +++ b/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+page.server.ts @@ -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, + } +} diff --git a/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+page.svelte b/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+page.svelte index e845566..929d498 100644 --- a/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+page.svelte +++ b/website/src/routes/packages/[scope]/[name]/[[version]]/[[target]]/+page.svelte @@ -1 +1,8 @@ -README + + +
+ + {@html data.readmeHtml} +
diff --git a/website/tailwind.config.ts b/website/tailwind.config.ts index 1f19abc..e6fb711 100644 --- a/website/tailwind.config.ts +++ b/website/tailwind.config.ts @@ -1,6 +1,8 @@ import type { Config } from "tailwindcss" import defaultTheme from "tailwindcss/defaultTheme" +const alpha = (color: string, alpha: number = 1) => color.replace("", alpha.toString()) + export default { content: ["./src/**/*.{html,js,svelte,ts}"], @@ -45,6 +47,28 @@ export default { borderColor: { DEFAULT: "rgb(var(--color-border) / )", }, + 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")), + }, + }, + }), }, },