feat: website

* feat(website): init

* feat(website): home page

* feat(website): make page more responsive

* feat(website): layout

* feat(website): package page

* feat(website): update PackageResponse type

* feat(website): display package readme

* feat(website): use new /latest/any endpoint

* feat(website): make website lg instead of xl

* fix(website): use NodeJS.Timeout

* feat(website): versions page

* feat(website): add latest version indicator

* feat(website): add target select menu

* feat(website): indicate current version

* feat(website): add package metadata

* feat(website): add hamburger

* fix(website): header responsiveness

* feat(website): better package layout

* feat(website): display authors on package page

* fix(website): only display relative dates on client

* feat(docs): init docs site

* chore(website): read .env from project root

* feat(website): add gemoji support

* fix(website): overflow on code blocks

* chore(docs): read .env from project root

* feat(docs): config changes

* fix: authors not displaying

* fix(website): use fallback language

* refactor(website): use predefined target names

* refactor(website): change Github to GitHub

* chore: remove starter readmes

* chore(docs): remove .vscode

* chore(docs): remove unused assets folder

* fix(website): fix missing datetime attribute

* feat(website): switch to universal loaders

* feat(docs): search

* fix(website): type errors

* fix(website): use provided fetch instead of global

* feat(website): remove isr

* chore(website): add .env.example

* feat(website): add icons and metadata

* chore(website): add debug logs

* chore(website): remove shiki temporarily

* fix(website): rehype shiki lazy load

* fix(website): use custom highlighter

* fix(website): move highlighter creation into load

* docs: write docs

* feat(website): add og image

* feat(website): fix accessibility issues

* fix(website): no target selector on mobile

* fix(website): close dialog on navigation

* fix(website): logo is not a link in hamburger menu

* feat(website): dependencies tab

* fix(website): use correct dependency target

* fix(website): navigation links

* feat(website): support wally dependencies

* feat(website): metadata + case insensitivity

* fix(website): manually implement groupBy

`Object.groupBy` isn't supported on Vercel right now.

* fix(website): code block with an unknown language

* docs(policies): explain & cover more cases

* docs: update cli reference

* docs: add self hosting registries guide

* docs: update README

* docs: add more configs to registry guide

* fix: favicon and logomark

* feat(website): package documentation

* fix(website): missing $derive for toc

* docs: change SENTRY_URL to SENTRY_DSN

* chore(website): remove unused file

* chore: remove favicon.zip

* fix(website): strip wally# prefix

* chore: add changelog entry

---------

Co-authored-by: daimond113 <72147841+daimond113@users.noreply.github.com>
This commit is contained in:
Luka 2024-10-29 20:06:00 +01:00 committed by GitHub
parent b1ae6aebda
commit f0d04fc87c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
111 changed files with 4969 additions and 15 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
PUBLIC_REGISTRY_URL= # url of the registry API, this must have a trailing slash and include the version
# example: https://registry.pesde.daimond113.com/v0/

View file

@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Added
- New website by @lukadev-0
### Fixed ### Fixed
- Use updated aliases when reusing lockfile dependencies by @daimond113 - Use updated aliases when reusing lockfile dependencies by @daimond113
- Listen for device flow completion without requiring pressing enter by @daimond113 - Listen for device flow completion without requiring pressing enter by @daimond113

View file

@ -6,21 +6,23 @@
<br> <br>
pesde is a package manager for the Luau programming language, supporting multiple runtimes including Roblox and Lune. pesde is a package manager for the Luau programming language, supporting
pesde has its own registry, however it can also use Wally, and Git repositories as package sources. multiple runtimes including Roblox and Lune. pesde has its own registry, however
It has been designed with multiple targets in mind, namely Roblox, Lune, and Luau. it can also use Wally, and Git repositories as package sources. It has been
designed with multiple targets in mind, namely Roblox, Lune, and Luau.
## Installation ## Installation
pesde can be installed from GitHub Releases. You can find the latest pesde can be installed from GitHub Releases. You can find the latest release
release [here](https://github.com/daimond113/pesde/releases). Once you have downloaded the binary, [here](https://github.com/daimond113/pesde/releases). Once you have downloaded
run the following command to install it: the binary, run the following command to install it:
```sh ```sh
pesde self-install pesde self-install
``` ```
Note that pesde manages its own versions, so you can update it by running the following command: Note that pesde manages its own versions, so you can update it by running the
following command:
```sh ```sh
pesde self-upgrade pesde self-upgrade
@ -28,19 +30,23 @@ pesde self-upgrade
## Documentation ## Documentation
For more information about its usage, you can check the [documentation](https://docs.pesde.daimond113.com). For more information about its usage, you can check the
[documentation](https://docs.pesde.daimond113.com).
*Currently waiting on [this PR](https://github.com/daimond113/pesde/pull/3) to be merged.*
## Registry ## Registry
The main pesde registry is hosted on [fly.io](https://fly.io). You can find it at https://registry.pesde.daimond113.com. The main pesde registry is hosted on [fly.io](https://fly.io). You can find it
at https://registry.pesde.daimond113.com.
### Self-hosting ### Self-hosting
The registry tries to require no modifications to be self-hosted. Please refer to the [example .env file](https://github.com/daimond113/pesde/blob/0.5/registry/.env.example) for more information. The registry tries to require no modifications to be self-hosted. Please refer
to the
[documentation](http://docs.pesde.daimond113.com/guides/self-hosting-registries)
for more information.
## Previous art ## Previous art
pesde is heavily inspired by [npm](https://www.npmjs.com/), [pnpm](https://pnpm.io/), [Wally](https://wally.run), pesde is heavily inspired by [npm](https://www.npmjs.com/),
and [Cargo](https://doc.rust-lang.org/cargo/). [pnpm](https://pnpm.io/), [Wally](https://wally.run), and
[Cargo](https://doc.rust-lang.org/cargo/).

3
assets/logomark.svg Normal file
View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6025 0L92.9038 25V75L49.6025 100L6.30127 75V25L49.6025 0ZM14.3013 29.6188L49.6025 9.2376L84.9038 29.6188V70.3812L49.6025 90.7624L33.6148 81.5319V67.3848C34.5167 68.5071 35.6388 69.4215 36.981 70.1279C38.9701 71.148 41.0357 71.658 43.1779 71.658C46.442 71.658 49.1452 70.8929 51.2873 69.3629C53.4805 67.7818 55.1126 65.7672 56.1836 63.319C57.0915 61.3382 57.632 59.274 57.8054 57.1263C59.8723 57.7457 62.2157 58.0554 64.8356 58.0554C67.6918 58.0554 70.3695 57.6473 72.8686 56.8313C75.3678 55.9642 77.4079 54.8167 78.989 53.3886L75.7758 47.8038C74.5517 48.9258 72.9961 49.8439 71.109 50.5579C69.2219 51.221 67.2073 51.5525 65.0652 51.5525C61.3929 51.5525 58.6643 50.6854 56.8792 48.9513C56.7195 48.7962 56.567 48.6365 56.4217 48.472C55.6102 47.5539 55.0211 46.4896 54.6546 45.2791L54.6443 45.2452L54.669 45.2791H79.2185V41.9894C79.2185 39.0313 78.5555 36.3536 77.2294 33.9565C75.9543 31.5593 74.0927 29.6467 71.6445 28.2186C69.2474 26.7395 66.3657 26 62.9995 26C59.6843 26 56.8027 26.7395 54.3545 28.2186C51.9064 29.6467 50.0193 31.5593 48.6932 33.9565C47.6743 35.7983 47.0469 37.8057 46.8108 39.9788C45.6888 39.728 44.4778 39.6026 43.1779 39.6026C41.0357 39.6026 38.9701 40.1127 36.981 41.1327C35.3162 41.9651 33.9902 43.1549 33.0028 44.7023V40.3677H20.6855V46.2585H25.8113V77.0266L14.3013 70.3812V29.6188ZM55.1961 36.0986C54.6528 37.1015 54.3321 38.1216 54.234 39.1588H71.7976C71.7976 38.0367 71.4405 36.9401 70.7265 35.8691C70.0634 34.747 69.0689 33.8035 67.7428 33.0384C66.4677 32.2734 64.8867 31.8908 62.9995 31.8908C61.1124 31.8908 59.5058 32.2989 58.1798 33.1149C56.9047 33.88 55.9101 34.8745 55.1961 36.0986ZM49.6451 51.5692C49.3076 50.6641 48.8381 49.871 48.2367 49.1898C48.0885 49.0219 47.9323 48.8609 47.7681 48.7067C46.085 47.0746 44.0449 46.2585 41.6478 46.2585C40.1177 46.2585 38.6131 46.5645 37.134 47.1766C35.8594 47.6773 34.6863 48.5438 33.6148 49.7759V61.47C34.6863 62.6664 35.8594 63.5378 37.134 64.084C38.6131 64.6961 40.1177 65.0021 41.6478 65.0021C44.0449 65.0021 46.085 64.1861 47.7681 62.554C49.4512 60.9219 50.2928 58.6012 50.2928 55.5921C50.2928 54.0679 50.0769 52.727 49.6451 51.5692Z" fill="#F19D1E"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

22
docs/.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
# build output
dist/
# generated types
.astro/
.vercel/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store

14
docs/.prettierrc Normal file
View file

@ -0,0 +1,14 @@
{
"useTabs": true,
"printWidth": 100,
"semi": false,
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

110
docs/astro.config.mjs Normal file
View file

@ -0,0 +1,110 @@
import starlight from "@astrojs/starlight"
import tailwind from "@astrojs/tailwind"
import { defineConfig } from "astro/config"
import vercel from "@astrojs/vercel/serverless"
// https://astro.build/config
export default defineConfig({
integrations: [
starlight({
title: "pesde docs",
social: {
github: "https://github.com/daimond113/pesde",
},
sidebar: [
{
label: "Intro",
items: [{ slug: "" }, { slug: "installation" }, { slug: "quickstart" }],
},
{
label: "Guides",
autogenerate: { directory: "guides" },
},
{
label: "Reference",
autogenerate: { directory: "reference" },
},
{
label: "Registry",
autogenerate: { directory: "registry" },
},
],
components: {
SiteTitle: "./src/components/SiteTitle.astro",
},
customCss: ["./src/tailwind.css", "@fontsource-variable/nunito-sans"],
favicon: "/favicon.ico",
head: [
{
tag: "meta",
attrs: {
name: "theme-color",
content: "#F19D1E",
},
},
{
tag: "meta",
attrs: {
property: "og:image",
content: "/favicon-48x48.png",
},
},
{
tag: "meta",
attrs: {
name: "twitter:card",
content: "summary",
},
},
{
tag: "link",
attrs: {
rel: "icon",
type: "image/png",
href: "/favicon-48x48.png",
sizes: "48x48",
},
},
{
tag: "link",
attrs: {
rel: "icon",
type: "image/svg+xml",
href: "/favicon.svg",
},
},
{
tag: "link",
attrs: {
rel: "apple-touch-icon",
sizes: "180x180",
href: "/apple-touch-icon.png",
},
},
{
tag: "meta",
attrs: {
name: "apple-mobile-web-app-title",
content: "pesde docs",
},
},
{
tag: "link",
attrs: {
rel: "manifest",
href: "/site.webmanifest",
},
},
],
}),
tailwind({
applyBaseStyles: false,
}),
],
vite: {
envDir: "..",
},
output: "hybrid",
adapter: vercel(),
})

BIN
docs/bun.lockb Executable file

Binary file not shown.

30
docs/package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "docs",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.3",
"@astrojs/starlight": "^0.28.2",
"@astrojs/starlight-tailwind": "^2.0.3",
"@astrojs/tailwind": "^5.1.1",
"@astrojs/vercel": "^7.8.1",
"@fontsource-variable/nunito-sans": "^5.1.0",
"@shikijs/rehype": "^1.21.0",
"astro": "^4.15.9",
"sharp": "^0.33.5",
"shiki": "^1.21.0",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2"
},
"devDependencies": {
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

3
docs/public/favicon.svg Normal file
View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6025 0L92.9038 25V75L49.6025 100L6.30127 75V25L49.6025 0ZM14.3013 29.6188L49.6025 9.2376L84.9038 29.6188V70.3812L49.6025 90.7624L33.6148 81.5319V67.3848C34.5167 68.5071 35.6388 69.4215 36.981 70.1279C38.9701 71.148 41.0357 71.658 43.1779 71.658C46.442 71.658 49.1452 70.8929 51.2873 69.3629C53.4805 67.7818 55.1126 65.7672 56.1836 63.319C57.0915 61.3382 57.632 59.274 57.8054 57.1263C59.8723 57.7457 62.2157 58.0554 64.8356 58.0554C67.6918 58.0554 70.3695 57.6473 72.8686 56.8313C75.3678 55.9642 77.4079 54.8167 78.989 53.3886L75.7758 47.8038C74.5517 48.9258 72.9961 49.8439 71.109 50.5579C69.2219 51.221 67.2073 51.5525 65.0652 51.5525C61.3929 51.5525 58.6643 50.6854 56.8792 48.9513C56.7195 48.7962 56.567 48.6365 56.4217 48.472C55.6102 47.5539 55.0211 46.4896 54.6546 45.2791L54.6443 45.2452L54.669 45.2791H79.2185V41.9894C79.2185 39.0313 78.5555 36.3536 77.2294 33.9565C75.9543 31.5593 74.0927 29.6467 71.6445 28.2186C69.2474 26.7395 66.3657 26 62.9995 26C59.6843 26 56.8027 26.7395 54.3545 28.2186C51.9064 29.6467 50.0193 31.5593 48.6932 33.9565C47.6743 35.7983 47.0469 37.8057 46.8108 39.9788C45.6888 39.728 44.4778 39.6026 43.1779 39.6026C41.0357 39.6026 38.9701 40.1127 36.981 41.1327C35.3162 41.9651 33.9902 43.1549 33.0028 44.7023V40.3677H20.6855V46.2585H25.8113V77.0266L14.3013 70.3812V29.6188ZM55.1961 36.0986C54.6528 37.1015 54.3321 38.1216 54.234 39.1588H71.7976C71.7976 38.0367 71.4405 36.9401 70.7265 35.8691C70.0634 34.747 69.0689 33.8035 67.7428 33.0384C66.4677 32.2734 64.8867 31.8908 62.9995 31.8908C61.1124 31.8908 59.5058 32.2989 58.1798 33.1149C56.9047 33.88 55.9101 34.8745 55.1961 36.0986ZM49.6451 51.5692C49.3076 50.6641 48.8381 49.871 48.2367 49.1898C48.0885 49.0219 47.9323 48.8609 47.7681 48.7067C46.085 47.0746 44.0449 46.2585 41.6478 46.2585C40.1177 46.2585 38.6131 46.5645 37.134 47.1766C35.8594 47.6773 34.6863 48.5438 33.6148 49.7759V61.47C34.6863 62.6664 35.8594 63.5378 37.134 64.084C38.6131 64.6961 40.1177 65.0021 41.6478 65.0021C44.0449 65.0021 46.085 64.1861 47.7681 62.554C49.4512 60.9219 50.2928 58.6012 50.2928 55.5921C50.2928 54.0679 50.0769 52.727 49.6451 51.5692Z" fill="#F19D1E"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,21 @@
{
"name": "pesde",
"short_name": "pesde",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#f19d1e",
"background_color": "#0a0704",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,30 @@
<div class="flex items-center">
<a
href="https://pesde.daimond113.com/"
class="flex text-[var(--sl-color-text-accent)] hover:opacity-80"
>
<svg viewBox="0 0 56 28" class="h-7" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>pesde</title>
<path
d="M0 28V26.3156H2.25652V12.2361H0.0635639V10.5517H4.44947L4.48125 11.9819L3.78205 12.3315C4.41769 11.6746 5.16986 11.1661 6.03857 10.8059C6.92846 10.4245 7.82895 10.2338 8.74003 10.2338C9.863 10.2338 10.88 10.4775 11.7911 10.9648C12.7234 11.4522 13.4544 12.1726 13.9841 13.126C14.5349 14.0795 14.8104 15.2448 14.8104 16.6221C14.8104 18.0416 14.5138 19.26 13.9205 20.277C13.3272 21.2728 12.5327 22.0356 11.5368 22.5653C10.5622 23.095 9.5028 23.3598 8.35865 23.3598C7.72301 23.3598 7.11916 23.2751 6.54708 23.1056C5.99619 22.9361 5.50887 22.7242 5.08511 22.4699C4.66135 22.1945 4.34353 21.8873 4.13165 21.5483L4.60838 21.4529L4.5766 26.3156H7.02381V28H0ZM7.94549 21.6118C9.19558 21.6118 10.2444 21.2092 11.0919 20.4041C11.9394 19.5778 12.3632 18.3807 12.3632 16.8127C12.3632 15.2872 11.9606 14.1113 11.1555 13.2849C10.3503 12.4586 9.3333 12.0454 8.1044 12.0454C7.72301 12.0454 7.26747 12.1196 6.73777 12.2679C6.20807 12.395 5.67837 12.6069 5.14867 12.9035C4.61898 13.2002 4.17403 13.5922 3.81383 14.0795L4.5766 12.7446L4.60838 20.7219L3.8774 19.7367C4.42828 20.3299 5.06392 20.7961 5.78431 21.1351C6.5047 21.4529 7.2251 21.6118 7.94549 21.6118Z"
fill="currentColor"></path>
<path
d="M18.37 17.7448C17.0563 17.7448 15.8592 17.4694 14.7786 16.9185C13.7192 16.3676 12.8717 15.5942 12.236 14.5984C11.6216 13.6026 11.3144 12.4478 11.3144 11.1341C11.3144 9.77811 11.611 8.61277 12.2043 7.63813C12.7975 6.64229 13.5921 5.87953 14.5879 5.34983C15.5837 4.79894 16.6961 4.5235 17.925 4.5235C19.1963 4.5235 20.2875 4.78835 21.1986 5.31805C22.1308 5.84775 22.8406 6.59992 23.3279 7.57456C23.8153 8.54921 24.0589 9.69336 24.0589 11.007V11.6109H13.3802L13.412 10.1489H21.7388C21.7388 9.32257 21.5693 8.62337 21.2303 8.05129C20.9125 7.45803 20.4676 7.01308 19.8955 6.71645C19.3234 6.39863 18.6666 6.23972 17.925 6.23972C17.1411 6.23972 16.4313 6.43042 15.7956 6.8118C15.16 7.17199 14.6515 7.70169 14.2701 8.4009C13.9099 9.07891 13.7298 9.90524 13.7298 10.8799C13.7298 11.9181 13.9205 12.8186 14.3019 13.5814C14.6833 14.3229 15.213 14.9056 15.891 15.3294C16.5902 15.732 17.3847 15.9332 18.2746 15.9332C19.2281 15.9332 20.1074 15.7425 20.9125 15.3612C21.7177 14.9798 22.4169 14.503 23.0101 13.931L23.8365 15.3612C23.2644 16.018 22.5228 16.5795 21.6117 17.0456C20.7006 17.5117 19.6201 17.7448 18.37 17.7448Z"
fill="currentColor"></path>
<path
d="M28.7199 22.6288C28.0631 22.6288 27.4275 22.5441 26.813 22.3746C26.2198 22.2051 25.7007 21.972 25.2557 21.6754C24.8108 21.3788 24.4718 21.0292 24.2387 20.6266L24.7154 20.9126V22.311H22.8721V17.9887H24.5247L24.7154 20.0545L24.2705 18.5608C24.5035 19.3447 25.0227 19.9486 25.8278 20.3723C26.6541 20.7961 27.5122 21.008 28.4021 21.008C29.2073 21.008 29.8747 20.8491 30.4044 20.5312C30.9553 20.2134 31.2307 19.7685 31.2307 19.1964C31.2307 18.5184 30.9977 18.0522 30.5315 17.798C30.0866 17.5225 29.3662 17.2789 28.3703 17.067L26.6223 16.6856C25.457 16.389 24.5353 15.997 23.8573 15.5097C23.1793 15.0012 22.8403 14.1642 22.8403 12.9989C22.8403 11.8759 23.2746 11.0178 24.1434 10.4245C25.0332 9.81009 26.135 9.50286 27.4487 9.50286C27.9572 9.50286 28.4869 9.57702 29.0378 9.72534C29.6098 9.85246 30.129 10.0538 30.5951 10.3292C31.0612 10.6046 31.3896 10.9436 31.5803 11.3462L31.1036 11.1873V9.75712H32.9787V14.0477H31.3261L30.9129 11.0284L31.4532 13.126C31.3684 12.7446 31.1248 12.4162 30.7222 12.1408C30.3408 11.8441 29.8853 11.6111 29.3556 11.4416C28.8471 11.2721 28.3386 11.1873 27.8301 11.1873C27.152 11.1873 26.5376 11.325 25.9867 11.6005C25.457 11.8759 25.1922 12.3209 25.1922 12.9353C25.1922 13.4015 25.3723 13.7617 25.7325 14.0159C26.1138 14.2702 26.7283 14.5033 27.5758 14.7151L29.1967 15.0647C30.0018 15.2554 30.7222 15.4885 31.3579 15.7639C32.0147 16.0182 32.5338 16.3996 32.9152 16.9081C33.2966 17.3954 33.4872 18.0946 33.4872 19.0057C33.4872 19.832 33.2542 20.5206 32.788 21.0715C32.3431 21.6012 31.7498 21.9932 31.0083 22.2475C30.2879 22.5017 29.5251 22.6288 28.7199 22.6288Z"
fill="currentColor"></path>
<path
d="M37.1104 18.5607C35.9662 18.5607 34.9068 18.3064 33.9322 17.7979C32.9787 17.2682 32.2054 16.5054 31.6121 15.5096C31.0188 14.5138 30.7222 13.3272 30.7222 11.95C30.7222 10.5304 30.9977 9.34389 31.5485 8.39043C32.1206 7.41579 32.8728 6.67421 33.8051 6.1657C34.7373 5.65719 35.7544 5.40293 36.8561 5.40293C37.746 5.40293 38.6253 5.57243 39.494 5.91144C40.3839 6.22926 41.1679 6.6848 41.8459 7.27807L41.0831 7.5641V1.65266H38.8584V0H43.435V16.5266H45.4055V18.2111H41.0831V16.5584L41.8141 16.6855C41.1997 17.2788 40.5005 17.7449 39.7165 18.0839C38.9537 18.4018 38.085 18.5607 37.1104 18.5607ZM37.6189 16.7809C38.4452 16.7809 39.208 16.622 39.9072 16.3042C40.6276 15.9863 41.2844 15.5626 41.8777 15.0329L41.0831 16.1135V7.85014L41.8777 8.99429C41.2208 8.42221 40.4793 7.97727 39.6529 7.65945C38.8478 7.34163 38.0744 7.18272 37.3329 7.18272C36.5277 7.18272 35.8073 7.37341 35.1717 7.75479C34.5572 8.13618 34.0699 8.69766 33.7097 9.43924C33.3495 10.1596 33.1694 11.0601 33.1694 12.1407C33.1694 13.1366 33.3707 13.9841 33.7733 14.6833C34.1759 15.3613 34.7056 15.8804 35.3624 16.2406C36.0404 16.6008 36.7926 16.7809 37.6189 16.7809Z"
fill="currentColor"></path>
<path
d="M50.3188 24.2004C49.0051 24.2004 47.808 23.925 46.7274 23.3741C45.668 22.8232 44.8205 22.0498 44.1848 21.054C43.5704 20.0582 43.2632 18.9034 43.2632 17.5898C43.2632 16.2337 43.5598 15.0684 44.1531 14.0937C44.7463 13.0979 45.5409 12.3351 46.5367 11.8054C47.5325 11.2545 48.6449 10.9791 49.8738 10.9791C51.1451 10.9791 52.2363 11.2439 53.1474 11.7736C54.0796 12.3033 54.7894 13.0555 55.2767 14.0302C55.7641 15.0048 56.0077 16.149 56.0077 17.4626V18.0665H45.329L45.3608 16.6045H53.6876C53.6876 15.7782 53.5181 15.079 53.1791 14.5069C52.8613 13.9136 52.4164 13.4687 51.8443 13.172C51.2722 12.8542 50.6154 12.6953 49.8738 12.6953C49.0899 12.6953 48.3801 12.886 47.7444 13.2674C47.1088 13.6276 46.6003 14.1573 46.2189 14.8565C45.8587 15.5345 45.6786 16.3609 45.6786 17.3355C45.6786 18.3737 45.8693 19.2742 46.2507 20.037C46.6321 20.7786 47.1617 21.3612 47.8398 21.785C48.539 22.1876 49.3335 22.3888 50.2234 22.3888C51.1769 22.3888 52.0562 22.1982 52.8613 21.8168C53.6665 21.4354 54.3657 20.9587 54.9589 20.3866L55.7853 21.8168C55.2132 22.4736 54.4716 23.0351 53.5605 23.5012C52.6494 23.9673 51.5688 24.2004 50.3188 24.2004Z"
fill="currentColor"></path>
</svg>
</a>
<span class="-mt-px ml-2.5 mr-2 text-xl text-[var(--sl-color-gray-5)]">/</span>
<a
class="font-medium text-[var(--sl-color-gray-2)] no-underline hover:opacity-80 md:text-lg"
href="/">docs</a
>
</div>

View file

@ -0,0 +1,6 @@
import { defineCollection } from 'astro:content';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ schema: docsSchema() }),
};

View file

@ -0,0 +1,41 @@
---
title: Using Binary Packages
description: Learn how to use binary packages.
---
A **binary package** is a package that contains a binary export.
Binary packages can be run like a normal program. There are several ways to use
binary packages with pesde.
## Using `pesde x`
The `pesde x` command can be used to run a one-off binary package. This is
useful for running a binary package without installing it or outside of a pesde
project.
```sh
pesde x pesde/hello
# Hello, pesde! (pesde/hello@1.0.0, lune)
```
## Installing a binary package
Binary packages can be installed using the `pesde add` and `pesde install`
commands.
This requires a `pesde.toml` file to be present in the current directory, and
will add the binary package to the `dependencies` section of the file.
```sh
pesde add pesde/hello
pesde install
```
This will add the binary package to your `PATH`, meaning that it can be run
anywhere!
```sh
hello
# Hello, pesde! (pesde/hello@1.0.0, lune)
```

View file

@ -0,0 +1,170 @@
---
title: Specifying Dependencies
description: Learn how to specify dependencies in your pesde project.
---
import { Aside, FileTree, LinkCard } from "@astrojs/starlight/components"
The `[dependencies]` section of your `pesde.toml` file is where you specify the
dependencies of your project.
pesde supports multiple types of dependencies.
## pesde Dependencies
The most common type of dependency are pesde dependencies. These are
dependencies on packages published to a [pesde registry](https://pesde.daimond113.com).
```toml title="pesde.toml"
[indices]
default = "https://github.com/daimond113/pesde-index"
[dependencies]
hello = { name = "pesde/hello", version = "^1.0.0" }
```
In this example, we're specifying a dependency on the `pesde/hello` package on
the official pesde registry with a version constraint of `^1.0.0`.
You can also add a dependency by running the following command:
```sh
pesde add pesde/hello
```
## Git Dependencies
Git dependencies are dependencies on packages hosted on a Git repository.
```toml title="pesde.toml"
[dependencies]
acme = { repo = "acme/package", rev = "main" }
```
In this example, we're specifying a dependency on the package contained within
the `acme/package` GitHub repository at the `main` branch.
You can also use a URL to specify the Git repository and a specific commit.
```toml title="pesde.toml"
[dependencies]
acme = { repo = "https://git.acme.local/package.git", rev = "aeff6" }
```
You can also specify a path if the package is not at the root of the repository.
<FileTree>
- acme/package.git
- pkgs/
- **foo/**
- pesde.toml
- ...
</FileTree>
```toml title="pesde.toml"
[dependencies]
foo = { repo = "acme/package", rev = "main", path = "pkgs/foo" }
```
The path specified by the Git dependency must either be a valid pesde package or
a [Wally][wally] package.
You can also add a Git dependency by running the following command:
```sh
# From Git URL
pesde add https://git.acme.local/package.git#aeff6
# From GitHub repository
pesde add gh#acme/package#main
```
## Wally Dependencies
Wally dependencies are dependencies on packages published to a
[Wally registry][wally]. Wally is a package manager for Roblox and thus Wally
dependencies should only be used in Roblox projects.
```toml title="pesde.toml"
[wally_indices]
default = "https://github.com/UpliftGames/wally-index"
[dependencies]
foo = { wally = "acme/package", version = "^1.0.0" }
```
In this example, we're specifying a dependency on the `acme/package` package
on the official Wally registry with a version constraint of `^1.0.0`.
<Aside type="note">
In order to get proper types support for Wally dependencies, you need to have
a [`sourcemap_generator` script](/reference/manifest#sourcemap_generator)
specified in your `pesde.toml` file.
</Aside>
You can also add a Wally dependency by running the following command:
```sh
pesde add wally#acme/package
```
[wally]: https://wally.run/
## Workspace Dependencies
Packages within a workspace can depend on each other. For example, if `foo`
and `bar` are both packages in the same workspace, you can add a dependency to
`bar` in the `foo/pesde.toml` file:
```toml title="foo/pesde.toml"
[dependencies]
bar = { workspace = "acme/bar", version = "^" }
```
You can also add a workspace dependency by running the following command:
```sh
pesde add workspace:acme/bar
```
<LinkCard
title="Workspaces"
description="Learn more about using workspaces in pesde."
href="/guides/workspaces/"
/>
## Peer Dependencies
Peer dependencies are dependencies that are not installed automatically when
used by another package. They need to be installed by the user of the package.
```toml title="pesde.toml"
[peer_dependencies]
foo = { name = "acme/foo", version = "^1.0.0" }
```
You can add a peer dependency by passing `--peer` to the `pesde add` command:
```sh
pesde add --peer acme/foo
```
## Dev Dependencies
Dev dependencies are dependencies that are only used during development. They
are not installed when the package is used as a dependency.
```toml title="pesde.toml"
[dev_dependencies]
foo = { name = "acme/foo", version = "^1.0.0" }
```
You can add a dev dependency by passing `--dev` to the `pesde add` command:
```sh
pesde add --dev acme/foo
```

View file

@ -0,0 +1,80 @@
---
title: Overriding Dependencies
description: Learn how to override and patch dependencies in pesde.
---
import { Aside } from '@astrojs/starlight/components'
pesde has several ways to override or patch dependencies in your project.
## Dependency Overrides
Dependency overrides allow you to replace a dependency of a dependency with a
different version or package.
Let's say you have a project with the following dependencies:
```toml title="pesde.toml"
[dependencies]
foo = { name = "acme/foo", version = "^1.0.0" }
```
But `foo` depends on `bar` 1.0.0, and you want to use `bar` 2.0.0 instead. You
can override the `bar` dependency in your `pesde.toml` file:
```toml title="pesde.toml"
[dependencies]
foo = { name = "acme/foo", version = "^1.0.0" }
[overrides]
"foo>bar" = { name = "acme/bar", version = "^2.0.0" }
```
Now, when you run `pesde install`, `bar` 2.0.0 will be used instead of 1.0.0.
You can learn more about the syntax for dependency overrides in the
[reference](/reference/manifest#overrides).
## Patching Dependencies
Patching allows you to modify the source code of a dependency.
To patch a dependency, you can use the `pesde patch` and `pesde patch-commit`
commands.
Let's say you have the following dependency in your `pesde.toml` file:
```toml title="pesde.toml"
[target]
environment = "luau"
[dependencies]
foo = { name = "acme/foo", version = "^1.0.0" }
```
And you want to patch `foo` to fix a bug. You can run the following command:
```sh
pesde patch "acme/foo@1.0.0 luau"
# done! modify the files in the directory, then run `pesde patch-commit /x/y/z`
# to apply.
# warning: do not commit these changes
# note: the pesde.toml file will be ignored when patching
```
pesde will copy the source code of `foo` to a temporary directory, in this case
`/x/y/z`. You can then modify the files in this directory. Once you're done,
run `pesde patch-commit /x/y/z` to apply the changes.
This will create a patch within the `patches` directory of your project, and
add an entry to `[patches]`. Then, next time you run `pesde install`, the patch
will be applied to the dependency.
<Aside type="caution">
Make sure not to commit or stage the changes made in the temporary directory.
Otherwise pesde may not be able to create the patch correctly.
</Aside>

View file

@ -0,0 +1,94 @@
---
title: Publishing Packages
description: Learn how to publish packages to the pesde registry.
---
## Configuration
Before you can publish a package, you must configure the required fields in your
`pesde.toml` file.
### `includes`
The `includes` field is a list of files and directories that should be included
in the package.
```toml
includes = [
"pesde.toml",
"README.md",
"LICENSE",
"init.luau",
]
```
### `target`
The `target` field defines the environment where the package can be run.
Here, you must also specify the `lib` and/or `bin` fields to indicate the path
of the exported library or binary.
```toml
[target]
environment = "luau"
lib = "init.luau"
```
#### Roblox
`bin` is not supported in Roblox packages. You must also specify a list of
`build_files`. These are the files that should be synced into Roblox. They are
passed to the `roblox_sync_config_generator` script.
For more information, see [Roblox](/guides/roblox).
```toml
[target]
environment = "roblox"
lib = "init.luau"
build_files = ["init.luau"]
```
## Authentication
Before you can publish a package, you must authenticate with your GitHub account.
```sh
pesde auth login
```
You will be given a code and prompted to open the GitHub authentication page in
your browser. You must enter the code to authenticate.
## Publishing
To publish a package, run the following command:
```sh
pesde publish
```
You will be prompted to confirm the package details before publishing.
Once a package is published, others will be able to install it. You may not
remove a package once it has been published. You may not publish a package with
an already existing version.
## Multi-target Packages
You may publish packages under the same name and version but with different
targets. This allows you to publish a package that can be used in multiple
environments.
For example, you may publish a package that can be used in both Roblox and
Luau environments by publishing two versions of the package, one for each
environment.
## Documentation
The `README.md` file in the root of the package will be displayed on the
[pesde registry website](https://pesde.daimond113.com/).
If you have a `docs` directory in the root of the package, they will be
hosted by pesde and be accessible on the pesde website.

View file

@ -0,0 +1,36 @@
---
title: Roblox
description: Using pesde in a Roblox project.
---
pesde can be used in Roblox projects, however this requires some extra setup.
Namely, you need to specify a `roblox_sync_config_generator` script in order
to generate the adequate configuration for the sync tool you are using.
The [`pesde-scripts`](https://github.com/daimond113/pesde-scripts)
repository contains a list of scripts for different sync tools. If the tool
you are using is not supported, you can write your own script and submit a PR
to get it added.
These scripts are automatically cloned into the `~/.pesde/scripts` folder and
kept up to date when you use pesde.
## Usage with Rojo
[Rojo](https://rojo.space/) is a popular tool for syncing files into Roblox
Studio.
Running `pesde init` will prompt you to select a target, select
`roblox` or `roblox_server` in this case. This will setup the configuration
needed to use pesde in a project using Rojo.
## Usage with other tools
If you are using a different sync tool, you should look for it's scripts in the
pesde-scripts repository. If you cannot find them, you can write your own and
optionally submit a PR to help others using the same tool as you get started
quicker.
Scaffold your project with `pesde init`, select the `roblox` or `roblox_server`
target, and then replace the `.pesde/roblox_sync_config_generator.luau` script
with the one you want to use.

View file

@ -0,0 +1,204 @@
---
title: Self Hosting Registries
description: Learn how to self host registries for pesde.
---
You can self host registries for pesde. This is useful if you want a private
registry or if you a separate registry for other reasons.
## Making the index repository
The index is a repository that contains metadata about all the packages in the
registry.
An index contains a `config.toml` file with configuration options.
To create an index, create a new repository and add a `config.toml` file with
the following content:
```toml title="config.toml"
# The URL of the registry API
api = "https://registry.acme.local/"
# Package download URL (optional)
download = "{API_URL}/v0/packages/{PACKAGE}/{PACKAGE_VERSION}/{PACKAGE_TARGET}"
# the client ID of the GitHub OAuth app (optional)
github_oauth_client_id = "a1d648966fdfbdcd9295"
# whether to allow packages with Git dependencies (default: false)
git_allowed = true
# whether to allow packages which depend on packages from other registries
# (default: false)
other_registries_allowed = true
# whether to allow packages with Wally dependencies (default: false)
wally_allowed = false
# the maximum size of the archive in bytes (default: 4MB)
max_archive_size = 4194304
```
- **api**: The URL of the registry API. See below for more information.
- **download**: The URL to download packages from. This is optional and
defaults to the correct URL for the official pesde registry implementation.
You only need this if you are using a custom registry implementation.
This string can contain the following placeholders:
- `{API_URL}`: The API URL (as specified in the `api` field).
- `{PACKAGE}`: The package name.
- `{PACKAGE_VERSION}`: The package version.
- `{PACKAGE_TARGET}`: The package target.
Defaults to `{API_URL}/v0/packages/{PACKAGE}/{PACKAGE_VERSION}/{PACKAGE_TARGET}`.
- **github_oauth_client_id**: This is required if you use GitHub OAuth for
authentication. See below for more information.
- **git_allowed**: Whether to allow packages with Git dependencies. This is
optional and defaults to `false`.
- **other_registries_allowed**: Whether to allow packages which depend on
packages from other registries. This is optional and defaults to `false`.
- **wally_allowed**: Whether to allow packages with Wally dependencies. This is
optional and defaults to `false`.
- **max_archive_size**: The maximum size of the archive in bytes. This is
optional and defaults to `4194304` (4MB).
You should then push this repository to [GitHub](https://github.com/).
## Configuring the registry
The registry is a web server that provides package downloads and the ability to
publish packages.
The official registry implementation is available in the
[pesde GitHub repository](https://github.com/daimond113/pesde/tree/0.5/registry).
Configuring the registry is done using environment variables. In order to allow
the registry to access the index repository, you must use a personal access
token of a GitHub account that has access to the index repository. We recommend
using a separate GitHub account for this purpose.
For instructions on how to create a personal access token, see the
[GitHub documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).
The access token must have read and write access to the index repository.
### General configuration
- **INDEX_REPO_URL**: The URL of the index repository. This is required.\
Example: `https://github.com/daimond113/pesde-index.git`
- **GITHUB_USERNAME**: The username of the GitHub account that has access to the
index repository. This is required.
- **GITHUB_PAT**: The personal access token of the GitHub account specified by
`GITHUB_USERNAME`. This is required.
- **COMMITTER_GIT_NAME**: The name to use for the committer when updating the
index repository.\
Example: `pesde index updater`
- **COMMITTER_GIT_EMAIL**: The email to use for the committer when updating the
index repository.\
Example: `pesde@localhost`
- **ADDRESS**: The address to bind the server to.\
Default: `127.0.0.1`
- **PORT**: The port to bind the server to.\
Default: `8080`
### Authentication configuration
The registry supports multiple authentication methods, which are documented
below.
#### General configuration
- **READ_NEEDS_AUTH**: If set to any value, reading data requires
authentication. If not set, anyone can read from the registry.
This is optional.
#### Single token authentication
Allows read and write access to the registry using a single token.
- **ACCESS_TOKEN**: The token to use for authentication.
#### Multiple token authentication
Allows read and write access to the registry using different tokens.
- **READ_ACCESS_TOKEN**: The token that grants read access.
- **WRITE_ACCESS_TOKEN**: The token that grants write access.
#### GitHub OAuth authentication
Allows clients to get read and write access to the registry using GitHub OAuth.
This requires a GitHub OAuth app, instructions to create one can be found
in the [GitHub documentation](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app).
- **GITHUB_CLIENT_SECRET**: The client secret of the GitHub OAuth app.
#### No authentication
If none of the above variables are set, **anyone** will be able to read and
write to the registry.
### Storage configuration
The registry supports multiple storage backends, which are documented below.
#### File system storage
Stores packages on the file system.
- **FS_STORAGE_ROOT**: The root directory where packages are stored.
#### S3 storage
Stores packages on an S3 compatible storage service, such as
[Amazon S3](https://aws.amazon.com/s3/) or
[Cloudflare R2](https://www.cloudflare.com/r2/).
- **S3_ENDPOINT**: The endpoint of the S3 bucket to store packages in.
- **S3_BUCKET_NAME**: The name of the bucket.
- **S3_REGION**: The region of the bucket.
- **S3_ACCESS_KEY**: The access key to use.
- **S3_SECRET_KEY**: The secret key to use.
### Sentry configuration
The registry supports [Sentry](https://sentry.io/) for error tracking.
- **SENTRY_DSN**: The DSN of the Sentry instance.
## Running the registry
First clone the repository and navigate to the repository directory:
```sh
git clone https://github.com/daimond113/pesde.git
cd pesde
```
You can then build the registry using the following command:
```sh
cargo build --release -p pesde-registry
```
This will build the registry. The resulting binary will be located at
`target/release/pesde-registry` or `target/release/pesde-registry.exe`.
After setting the environment variables, you can run the registry using the
by executing the binary.
The registry must be exposed at the URL specified in the `api` field of the
index repository configuration.

View file

@ -0,0 +1,100 @@
---
title: Workspaces
description: Learn how to use workspaces in pesde.
---
import { FileTree, LinkCard } from "@astrojs/starlight/components"
Workspaces allow you to work with multiple pesde projects within a single
repository. Packages within a workspace can depend on each other. And you can
run commands like install or publish on every package in the workspace at once.
Let's say you have a repository with the following structure:
<FileTree>
- pesde.toml
- pkgs/
- foo/
- pesde.toml
- ...
- bar/
- pesde.toml
- ...
</FileTree>
Within the root `pesde.toml` file, we can define a workspace:
```toml title="pesde.toml"
name = "acme/root"
version = "0.0.0"
private = "true"
workspace_members = ["pkgs/*"]
[target]
environment = "luau"
```
Now, each folder within the `pkgs/` directory is considered a package in the
workspace. You can run commands like `pesde install` or `pesde publish` from
the root of the repository to run them on every package in the workspace.
## Workspace Dependencies
Packages within a workspace can depend on each other. For example, if `foo`
depends on `bar`, you can add a dependency to `bar` in the `foo/pesde.toml` file:
```toml title="pkgs/foo/pesde.toml"
name = "acme/foo"
version = "1.0.0"
[dependencies]
bar = { workspace = "acme/bar", version = "^" }
```
Workspace dependencies are replaced with normal pesde dependencies when
publishing.
The `version` field can either contain `^`, `*`, `=`, `~`, or a specific version
requirement, such as `^1.0.0`. If you use `^`, `=`, or `~`, it will be replaced
with the version of the package in the workspace when publishing.
For example, if you had the following:
```toml title="pesde.toml"
[dependencies]
bar = { workspace = "acme/bar", version = "^" }
qux = { workspace = "acme/qux", version = "=" }
qar = { workspace = "acme/qar", version = "~" }
zoo = { workspace = "acme/zoo", version = "^2.1.0" }
baz = { workspace = "acme/baz", version = "*" }
```
If `bar`, `baz`, `qux`, `qar`, and `zoo` are all at version `2.1.5` in the
workspace, the `pesde.toml` file will be transformed into the following when
publishing.
```toml title="pesde.toml"
[dependencies]
bar = { name = "acme/bar", version = "^2.1.5" }
qux = { name = "acme/qux", version = "=2.1.5" }
qar = { name = "acme/qar", version = "~2.1.5" }
zoo = { name = "acme/zoo", version = "^2.1.0" }
baz = { name = "acme/baz", version = "*" }
```
A `target` field can be added to the `dependencies` table to specify a target
environment for the dependency.
```toml title="pesde.toml"
[dependencies]
bar = { workspace = "acme/bar", version = "^", target = "luau" }
```
<LinkCard
title="Specifying Dependencies"
description="Learn more about specifying dependencies in pesde."
href="/guides/dependencies/"
/>

View file

@ -0,0 +1,32 @@
---
title: What is pesde?
description: A package manager for the Luau programming language, supporting multiple runtimes including Roblox and Lune.
---
pesde is a package manager for the Luau programming language.
## Why use pesde?
When you write code, you often want to use libraries or frameworks that others
have written. Manually downloading and managing these can be cumbersome.
These libraries or frameworks can be distributed as packages. You can then
easily install and use these packages using pesde. pesde will automatically
download and manage the packages, and their dependencies, for you.
## Multi-target support
Luau can run in a lot of different places, such as on [Roblox][roblox], or in
[Lune][lune].
pesde is designed to work with all of these runtimes. Packages can publish
multiple versions of themselves, each tailored to a specific runtime.
[registry]: https://pesde.daimond113.com/
[roblox]: https://www.roblox.com/
[lune]: https://lune-org.github.io/docs
## The pesde registry
The [pesde registry][registry] is where anyone can publish their packages for
others to use.

View file

@ -0,0 +1,89 @@
---
title: Installation
description: Install pesde
---
import { Aside, Steps, TabItem, Tabs } from "@astrojs/starlight/components"
## Prerequisites
pesde requires [Lune](https://lune-org.github.io/docs) to be installed on your
system in order to function properly.
You can follow the installation instructions in the
[Lune documentation](https://lune-org.github.io/docs/getting-started/1-installation).
## Installing pesde
<Steps>
1. Go to the [GitHub releases page](https://github.com/daimond113/pesde/releases/latest).
2. Download the corresponding archive for your operating system. You can choose
whether to use the `.zip` or `.tar.gz` files.
3. Extract the downloaded archive to a folder on your computer.
4. Open a terminal and locate the path of the extracted `pesde` binary.
<Tabs syncKey="os">
<TabItem label="Windows">
If you extracted the archive to `C:\Users\User\Downloads`, the path to the
`pesde` binary would be `C:\Users\User\Downloads\pesde.exe`.
You can then run the `self-install` command:
```ps
C:\Users\User\Downloads\pesde.exe self-install
```
pesde should now be installed on your system. You may need to restart your
computer for the changes to take effect.
</TabItem>
<TabItem label="Linux & macOS">
If you extracted the archive to `~/Downloads`, the path to the `pesde`
binary would be `~/Downloads/pesde`.
You must then add execute permissions and run the `self-install` command:
```sh
chmod +x ~/Downloads/pesde
~/Downloads/pesde self-install
```
pesde should now be installed on your system. You will need to update your
shell configuration file to add the pesde binary to your `PATH`
environment variable.
```sh title=".zshrc"
export PATH = "$PATH:/home/user/.pesde/bin"
```
You should then be able to run `pesde` after restarting your shell.
</TabItem>
</Tabs>
5. Verify that pesde is installed by running the following command:
```sh
pesde -v
```
This command should output the version of pesde that you installed.
</Steps>
<Aside type="caution">
It is not recommended to use toolchain managers (such as Rokit or Aftman) to
install pesde. You can use `pesde self-upgrade` if you need to update pesde.
If you need everyone to use the same version of pesde, you can use the
`pesde_version` field in `pesde.toml` to specify the version of pesde to use
for the current project.
</Aside>

View file

@ -0,0 +1,142 @@
---
title: Quickstart
description: Start using pesde
---
import { FileTree } from "@astrojs/starlight/components"
Let's make a simple Luau program that uses the `pesde/hello` package to print
hello to the terminal.
## Scaffolding the project
In your terminal, run the following commands to create a folder and navigate
into it.
```sh
mkdir hello-pesde
cd hello-pesde
```
Then, we'll use `pesde init` to scaffold a new pesde project. The command will
ask you a few questions to set up the project. Our project will be named
`<username>/hello_pesde`, replace `<username>` with a username of your choice.
The name may only contain lowercase letters, numbers, and underscores. The
environment we're targeting is `luau`.
```sh
pesde init
# What is the name of the project? <username>/hello_pesde
# What is the description of the project? (leave empty for none)
# Who are the authors of this project? (leave empty for none, comma separated)
# What is the repository URL of this project? (leave empty for none)
# What is the license of this project? (leave empty for none) MIT
# What environment are you targeting for your package? luau
# Would you like to setup a default roblox_sync_config_generator script? No
```
The command will create a `pesde.toml` file in the current folder. Go ahead
and open this file in your text editor of choice.
## Adding a main script
Under the `[target]` section, we're going to add a `bin` field to specify
the path to the main script of our package.
```diff lang="toml" title="pesde.toml"
name = "<username>/hello_pesde"
version = "0.1.0"
license = "MIT"
[target]
environment = "luau"
+ bin = "main.luau"
[indices]
default = "https://github.com/daimond113/pesde-index"
```
Don't forget to save the file after making the changes.
Now, lets create a `main.luau` file in the project folder and add the following
code to it.
```luau title="main.luau"
print("Hello, pesde!")
```
## Running the script
Then, we can run the following command to run the script.
```sh
pesde run
```
You should see `Hello, pesde!` printed to the terminal.
## Install a dependency
Let's use the `pesde/hello` package instead of printing ourselves.
Run the following command to add the package to `pesde.toml`.
```sh
pesde add pesde/hello
```
You should see that `pesde.toml` has been updated with the new dependency.
```diff lang="toml" title="pesde.toml"
name = "lukadev_0/hello_pesde"
version = "0.1.0"
license = "MIT"
[target]
environment = "luau"
bin = "main.luau"
[indices]
default = "https://github.com/daimond113/pesde-index"
+ [dependencies]
+ hello = { name = "pesde/hello", version = "^1.0.0" }
```
Run the following command to install the new dependency.
```sh
pesde install
```
You should see that pesde has created a `luau_packages` folder containing the
newly installed package. It has alsoo created a `pesde.lock` file, this file
contains the exact versions of the dependencies that were installed so that
they can be installed again in the future.
<FileTree>
- luau_packages/
- hello.luau
- ...
- main.luau
- pesde.lock
- pesde.toml
</FileTree>
Let's update the `main.luau` file to use the `pesde/hello` package.
```luau title="main.luau"
local hello = require("./luau_packages/hello")
hello()
```
If we run the script again, we should see something printed to the terminal.
```sh
pesde run
# Hello, pesde! (pesde/hello@1.0.0, luau)
```

View file

@ -0,0 +1,188 @@
---
title: pesde CLI
description: Reference for the pesde CLI.
---
import { LinkCard } from "@astrojs/starlight/components"
The pesde CLI is the primary way to interact with pesde projects. It provides
commands for installing dependencies, running scripts, and more.
## `pesde auth`
Authentication-related commands.
- `-i, --index`: The index of which token to manipulate. May be a URL or an alias.
Defaults to the default
index of the current project or the default index set in the config.
### `pesde auth login`
Sets the token for the index.
- `-t, --token`: The token to set.
If no token is provided, you will be prompted to authenticate with GitHub. A
code will be provided that you can paste into the GitHub authentication prompt.
### `pesde auth logout`
Removes the stored token for the index.
### `pesde auth whoami`
Prints the username of the currently authenticated user of the index. Only
works if the token is a GitHub token.
## `pesde config`
Configuration-related commands.
### `pesde config default-index`
```sh
pesde config default-index [INDEX]
```
Configures the default index. If no index is provided, the current default index
is printed.
- `-r, --reset`: Resets the default index.
The default index is [`pesde-index`](https://github.com/daimond113/pesde-index).
### `pesde config scripts-repo`
```sh
pesde config scripts-repo [REPO]
```
Configures the scripts repository. If no repository is provided, the current
scripts repository is printed.
- `-r, --reset`: Resets the scripts repository.
The default scripts repository is [`pesde-scripts`](https://github.com/daimond113/pesde-scripts).
## `pesde init`
Initializes a new pesde project in the current directory.
## `pesde run`
Runs a script from the current project using Lune.
```sh
pesde run [SCRIPT] [ -- <ARGS>...]
```
If no script is provided, it will run the script specified by `target.bin`
in `pesde.toml`.
If a path is provided, it will run the script at that path.
If a script defined in `[scripts]` is provided, it will run that script.
If a package name is provided, it will run the script specified by `target.bin`
in that package.
Arguments can be passed to the script by using `--` followed by the arguments.
```sh
pesde run foo -- --arg1 --arg2
```
## `pesde install`
Installs dependencies for the current project.
- `-t, --threads`: The number of threads to use for downloading dependencies.
- `--locked`: Whether to error if the lockfile is out of date.
- `--prod`: Whether to skip installing dev dependencies.
## `pesde publish`
Publishes the current project to the pesde registry.
- `-d, --dry-run`: Whether to perform a dry run. This will output a
tarball containing the package that would be published, but will not actually
publish it.
- `-y, --yes`: Whether to skip the confirmation prompt.
## `pesde self-install`
Performs the pesde installation process. This should be the first command run
after downloading the pesde binary.
## `pesde self-upgrade`
Upgrades the pesde binary to the latest version.
## `pesde patch`
```sh
pesde patch <PACKAGE>
```
Prepares a patching environment for a package. This will copy the source code of
the package to a temporary directory.
The package specified must be in the format `<name>@<version> <target>`.
<LinkCard
title="Overrides"
description="Learn more about overriding and patching packages."
href="/guides/overrides/"
/>
## `pesde patch-commit`
```sh
pesde patch-commit <PATH>
```
Applies the changes made in the patching environment created by `pesde patch`.
## `pesde add`
```sh
pesde add <PACKAGE>
```
Adds a package to the dependencies of the current project.
- `-i, --index <INDEX>`: The index in which to search for the package.
- `-t, --target <TARGET>`: The target environment for the package.
- `-a, --alias <ALIAS>`: The alias to use for the package, defaults to the
package name.
- `-p, --peer`: Adds the package as a peer dependency.
- `-d, --dev`: Adds the package as a dev dependency.
The following formats are supported:
```sh
pesde add pesde/hello
pesde add gh#acme/package#main
pesde add https://git.acme.local/package.git#aeff6
```
## `pesde update`
Updates the dependencies of the current project.
- `-t, --threads`: The number of threads to use for downloading dependencies.
## `pesde x`
Runs a one-off binary package.
```sh
pesde x <PACKAGE>
```
This is useful for running a binary package without installing it or outside of
a pesde project.
```sh
pesde x pesde/hello
```

View file

@ -0,0 +1,405 @@
---
title: pesde.toml
description: Reference for `pesde.toml`
---
import { LinkCard } from "@astrojs/starlight/components"
`pesde.toml` is the manifest file for a pesde package. It contains metadata about
the package and its dependencies.
## Top-level fields
```toml
name = "acme/package"
version = "1.2.3"
description = "A package that does foo and bar"
license = "MIT"
authors = ["John Doe <john.doe@acme.local> (https://acme.local)"]
repository = "https://github.com/acme/package"
```
### `name`
The name of the package. This is used to identify the package in the registry.
The name consists of a scope and a package name, separated by a slash (`/`). It
may only contain lowercase letters, numbers, and underscores.
The first one to publish to a given scope gets to own it. If you want multiple
people to be able to publish to the same scope, you can send a pull request to
the [pesde-index GitHub repository](https://github.com/daimond113/pesde-index)
and add the GitHub user ID of the other person to the `owners` field of the
`scope.toml` file of the given scope. For more information, see
[policies](/registry/policies#package-ownership).
### `version`
The version of the package. This must be a valid [SemVer](https://semver.org/)
version, such as `1.2.3`.
### `description`
A short description of the package. This is displayed on the package page in the
registry.
### `license`
The license of the package. It is recommended to use a
[SPDX license identifier](https://spdx.org/licenses/), such as `MIT` or
`Apache-2.0`.
### `authors`
A list of authors of the package. Each author is a string containing the name of
the author, optionally followed by an email address in angle brackets, and a
website URL in parentheses. For example:
```toml
authors = ["John Doe <john.doe@acme.local> (https://acme.local)"]
```
### `repository`
The URL of the repository where the package is hosted. This is displayed on the
package page in the registry.
### `private`
A boolean indicating whether the package is private. If set to `true`, the
package cannot be published to the registry.
### `includes`
List of top-level files and directories to include in the package when
publishing. Files not listed here will not be published.
```toml
includes = [
"pesde.toml",
"README.md",
"LICENSE",
"init.luau",
"docs",
]
```
### `pesde_version`
The version of pesde to use within this project. The `pesde` CLI will look at
this field and run the correct version of pesde for this project.
### `workspace_members`
A list of globs containing the members of this workspace.
<LinkCard
title="Workspaces"
description="Learn more about workspaces in pesde."
href="/guides/workspaces/"
/>
## `[target]`
The `[target]` section contains information about the target platform for the
package.
```toml
[target]
environment = "luau"
lib = "init.luau"
```
### `environment`
The target environment for the package. This can be one of the following:
- `luau`: Standalone Luau code that can be run using the `luau` CLI.
- `lune`: Luau code that requires the Lune runtime.
- `roblox`: Luau code that must be run in Roblox.
- `roblox_server`: Same as `roblox`, but only for server-side code.
### `lib`
**Allowed in:** `luau`, `lune`, `roblox`, `roblox_server`
The entry point of the library exported by the package. This file is what will
be required when the package is loaded using `require`.
### `bin`
**Allowed in:** `luau`, `lune`
The entry point of the binary exported by the package. This file is what will be
run when the package is executed as a binary.
<LinkCard
title="Using Binary Packages"
description="Learn more about using binary packages in pesde."
href="/guides/binary-packages/"
/>
### `build_files`
**Allowed in:** `roblox`, `roblox_server`
A list of files that should be synced to Roblox when the package is installed.
```toml
build_files = [
"init.luau",
"foo.luau",
]
```
These files are passed to [`roblox_sync_config_generator`](#roblox_sync_config_generator)
when the package is installed in order to generate the necessary configuration.
## `[scripts]`
The `[scripts]` section contains scripts that can be run using the `pesde run`
command. These scripts are run using [Lune](https://lune-org.github.io/docs).
```toml
[scripts]
build = "sripts/build.luau"
test = "scripts/test.luau"
```
There are also a few special scripts that are run in certain cases by pesde.
### `roblox_sync_config_generator`
This is responsible for generating adequate configuration files for Roblox
sync tools.
`process.args` will contain the directory containing the package, and the list
of files specified within the [`target.build_files`](#build_files) of the
package.
You can find template scripts inside the
[`pesde-scripts` repository](https://github.com/daimond113/pesde-scripts)
for various sync tools.
<LinkCard
title="Roblox"
description="Learn more about using pesde in Roblox projects."
href="/guides/roblox/"
/>
<LinkCard
title="Example script for Rojo"
description="An example script for generating configuration for Rojo."
href="https://github.com/daimond113/pesde-scripts/blob/master/lune/rojo/roblox_sync_config_generator.luau"
/>
### `sourcemap_generator`
This is responsible for generating source maps for packages that are installed.
This is required to get proper types support when using
[Wally dependencies](/guides/dependencies/#wally-dependencies).
The script will receive the path to the package directory as the first argument
through `process.args`.
<LinkCard
title="Example script for Rojo"
description="An example script for generating configuration for Rojo."
href="https://github.com/daimond113/pesde-scripts/blob/master/lune/rojo/sourcemap_generator.luau"
/>
## `[indices]`
The `[indices]` section contains a list of pesde indices where packages can be
installed from.
```toml
[indices]
default = "https://github.com/daimond113/pesde-index"
acme = "https://github.com/acme/pesde-index"
```
These can then be referenced in the [`dependencies`](#dependencies) of the
package. The `default` index is used if no index is specified.
```toml
[dependencies]
foo = { name = "acme/foo", version = "1.2.3", index = "acme" }
```
## `[wally_indices]`
The `[wally_indices]` section contains a list of Wally indices where packages
can be installed from. This is used for
[Wally dependencies](/guides/dependencies/#wally-dependencies).
```toml
[wally_indices]
default = "https://github.com/UpliftGames/wally-index"
acme = "https://github.com/acme/wally-index"
```
These can then be referenced in the [`dependencies`](#dependencies) of the
package. The `default` index is used if no index is specified.
```toml
[dependencies]
foo = { wally = "acme/foo", version = "1.2.3", index = "acme" }
```
## `[overrides]`
The `[overrides]` section contains a list of overrides for dependencies. This
allows you to replace certain dependencies with different versions or even
different packages.
```toml
[overrides]
"bar>baz" = { name = "acme/baz", version = "1.0.0" }
"foo>bar,baz>bar" = { name = "acme/bar", version = "2.0.0" }
```
The above example will replace the `baz` dependency of the `bar` package with
version `1.0.0`, and the `bar` and `baz` dependencies of the `foo` package with
version `2.0.0`.
Each key in the overrides table is a comma-separated list of package paths. The
path is a list of package names separated by `>`. For example, `foo>bar>baz`
refers to the `baz` dependency of the `bar` package, which is a dependency of
the `foo` package.
<LinkCard
title="Overrides"
description="Learn more about overriding and patching packages."
href="/guides/overrides/"
/>
## `[patches]`
The `[patches]` section contains a list of patches for dependencies. This allows
you to modify the source code of dependencies.
```toml
[patches]
"acme/foo" = { "1.0.0 luau" = "patches/acme+foo-1.0.0+luau.patch" }
```
The above example will patch version `1.0.0` with the `luau` target of the
`acme/foo` package using the `patches/acme+foo-1.0.0+luau.patch` file.
Each key in the patches table is the package name, and the value is a table
where the keys are the version and target, and the value is the path to the
patch.
The patches can be generated using the `pesde patch` command.
<LinkCard
title="Overrides"
description="Learn more about overriding and patching packages."
href="/guides/overrides/"
/>
## `[place]`
This is used in Roblox projects to specify where packages are located in the
Roblox datamodel.
```toml
[place]
shared = "game.ReplicatedStorage.Packages"
server = "game.ServerScriptService.Packages"
```
## `[dependencies]`
The `[dependencies]` section contains a list of dependencies for the package.
```toml
[dependencies]
foo = { name = "acme/foo", version = "1.2.3" }
bar = { wally = "acme/bar", version = "2.3.4" }
baz = { git = "acme/baz", rev = "main" }
```
Each key in the dependencies table is the name of the dependency, and the value
is a dependency specifier.
There are several types of dependency specifiers.
### pesde
```toml
[dependencies]
foo = { name = "acme/foo", version = "1.2.3", index = "acme", target = "lune" }
```
**pesde dependencies** contain the following fields:
- `name`: The name of the package.
- `version`: The version of the package.
- `index`: The [pesde index](#indices) to install the package from. If not
specified, the `default` index is used.
- `target`: The target platform for the package. If not specified, the target
platform of the current package is used.
### Wally
```toml
[dependencies]
foo = { wally = "acme/foo", version = "1.2.3", index = "acme" }
```
**Wally dependencies** contain the following fields:
- `wally`: The name of the package.
- `version`: The version of the package.
- `index`: The [Wally index](#wally_indices) to install the package from. If not
specified, the `default` index is used.
### Git
```toml
[dependencies]
foo = { git = "acme/packages", rev = "main", path = "foo" }
```
**Git dependencies** contain the following fields:
- `git`: The URL of the Git repository.
This can either be `<owner>/<name>` for a GitHub repository, or a full URL.
- `rev`: The Git revision to install. This can be a branch, tag, or commit hash.
- `path`: The path within the repository to install. If not specified, the root
of the repository is used.
## `[peer_dependencies]`
The `[peer_dependencies]` section contains a list of peer dependencies for the
package. These are dependencies that are required by the package, but are not
installed automatically. Instead, they must be installed by the user of the
package.
```toml
[peer_dependencies]
foo = { name = "acme/foo", version = "1.2.3" }
```
## `[dev_dependencies]`
The `[dev_dependencies]` section contains a list of development dependencies for
the package. These are dependencies that are only required during development,
such as testing libraries or build tools. They are not installed when the
package is used by another package.
```toml
[dev_dependencies]
foo = { name = "acme/foo", version = "1.2.3" }
```
<br/>
<LinkCard
title="Specifying Dependencies"
description="Learn more about specifying dependencies in pesde."
href="/guides/dependencies/"
/>

View file

@ -0,0 +1,96 @@
---
title: Policies
description: Policies for the pesde registry
---
The following policies apply to the [official public pesde registry](https://registry.pesde.daimond113.com)
and its related services, such as the index repository or websites.
They may not apply to other registries. By using the pesde registry, you agree
to these policies.
If anything is unclear, please [contact us](#contact-us), and we will be happy
to help.
## Contact Us
You can contact us at [pesde@daimond113.com](mailto:pesde@daimond113.com). In
case of a security issue, please prefix the subject with `[SECURITY]`.
## Permitted content
The pesde registry is a place for Luau-related packages. This includes:
- Libraries
- Frameworks
- Tools
The following content is forbidden:
- Malicious, vulnerable code
- Illegal, harmful content
- Miscellaneous files (doesn't include configuration files, documentation, etc.)
pesde is not responsible for the content of packages, the scope owner is. It
is the responsibility of the scope owner to ensure that the content of their
packages is compliant with the permitted content policy.
If you believe a package is breaking these requirements, please [contact us](#contact-us).
## Package removal
pesde does not support removing packages for reasons such as abandonment. A
package may only be removed for the following reasons:
- The package is breaking the permitted content policy
- The package contains security vulnerabilities
- The package must be removed for legal reasons (e.g. DMCA takedown)
In case a secret has been published to the registry, it must be invalidated.
If you believe a package should be removed, please [contact us](#contact-us).
We will review your request and take action if necessary.
If we find that a package is breaking the permitted content policy, we will
exercise our right to remove it from the registry without notice.
pesde reserves the right to remove any package from the registry at any time for
any or no reason, without notice.
## Package ownership
Packages are owned by scopes. Scope ownership is determined by the first person
to publish a package to the scope. The owner of the scope may send a pull request
to the index repository adding team members' user IDs to the scope's `scope.toml`
file to give them access to the scope, however at least one package must be
published to the scope before this can be done. The owner may also remove team
members from the scope.
A scope's true owner's ID must appear first in the `owners` field of the scope's
`scope.toml` file. Ownership may be transferred by the current owner sending a
pull request to the index repository, and the new owner confirming the transfer.
Only the owner may add or remove team members from the scope.
pesde reserves the right to override scope ownership in the case of a dispute,
such as if the original owner is unresponsive or multiple parties claim ownership.
## Scope squatting
Scope squatting is the act of creating a scope with the intent of preventing
others from using it, without any intention of using it yourself. This is
forbidden and can result in the removal (release) of the scope and its packages
from the registry without notice.
If you believe a scope is being squatted, please [contact us](#contact-us).
We will review your request and take action if necessary.
## API Usage
The pesde registry has an API for querying, downloading, and publishing packages.
Only non-malicious use is permitted. Malicious uses include:
- **Service Degradation**: this includes sending an excessive amount of requests
to the registry in order to degrade the service
- **Exploitation**: this includes trying to break the security of the registry
in order to gain unauthorized access
- **Harmful content**: this includes publishing harmful (non-law compliant,
purposefully insecure) content

2
docs/src/env.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

11
docs/src/tailwind.css Normal file
View file

@ -0,0 +1,11 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root[data-theme="light"] {
--sl-color-bg: rgb(255 245 230);
}
:root[data-theme="light"] .sidebar-pane {
background-color: var(--sl-color-bg);
}

36
docs/tailwind.config.ts Normal file
View file

@ -0,0 +1,36 @@
import starlightPlugin from "@astrojs/starlight-tailwind"
import type { Config } from "tailwindcss"
import defaultTheme from "tailwindcss/defaultTheme"
export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {
fontFamily: {
sans: ["Nunito Sans Variable", ...defaultTheme.fontFamily.sans],
},
colors: {
accent: {
200: "rgb(241 157 30)",
600: "rgb(120 70 10)",
900: "rgb(24 16 8)",
950: "rgb(10 7 4)",
},
gray: {
100: "rgb(245 230 210)",
200: "rgb(228 212 192)",
300: "rgb(198 167 140)",
400: "rgb(142 128 112)",
500: "rgb(84 70 50)",
600: "rgb(65 50 41)",
700: "rgb(50 42 35)",
800: "rgb(28 22 17)",
900: "rgb(10 7 4)",
},
},
},
},
plugins: [starlightPlugin()],
} as Config

3
docs/tsconfig.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}

View file

@ -61,7 +61,7 @@ const ADDITIONAL_FORBIDDEN_FILES: &[&str] = &["default.project.json"];
struct DocEntryInfo { struct DocEntryInfo {
#[serde(default)] #[serde(default)]
label: Option<String>, label: Option<String>,
#[serde(default)] #[serde(default, alias = "position")]
sidebar_position: Option<usize>, sidebar_position: Option<usize>,
#[serde(default)] #[serde(default)]
collapsed: bool, collapsed: bool,

21
website/.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
website/.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

4
website/.prettierignore Normal file
View file

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

14
website/.prettierrc Normal file
View file

@ -0,0 +1,14 @@
{
"useTabs": true,
"printWidth": 100,
"semi": false,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

BIN
website/bun.lockb Executable file

Binary file not shown.

33
website/eslint.config.js Normal file
View file

@ -0,0 +1,33 @@
import js from "@eslint/js"
import ts from "typescript-eslint"
import svelte from "eslint-plugin-svelte"
import prettier from "eslint-config-prettier"
import globals from "globals"
/** @type {import('eslint').Linter.Config[]} */
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs["flat/recommended"],
prettier,
...svelte.configs["flat/prettier"],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
files: ["**/*.svelte"],
languageOptions: {
parserOptions: {
parser: ts.parser,
},
},
},
{
ignores: ["build/", ".svelte-kit/", "dist/"],
},
]

70
website/package.json Normal file
View file

@ -0,0 +1,70 @@
{
"name": "website",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-vercel": "^5.4.6",
"@sveltejs/kit": "^2.7.3",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/typography": "^0.5.15",
"@types/eslint": "^9.6.1",
"@types/gunzip-maybe": "^1.4.2",
"@types/tar-stream": "^3.1.3",
"autoprefixer": "^10.4.20",
"eslint": "^9.13.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.0",
"globals": "^15.11.0",
"mdsvex": "^0.12.3",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"prettier-plugin-tailwindcss": "^0.6.8",
"svelte": "^5.1.4",
"svelte-check": "^4.0.5",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"typescript-eslint": "^8.12.2",
"vite": "^5.4.10"
},
"type": "module",
"dependencies": {
"@fontsource-variable/nunito-sans": "^5.1.0",
"@shikijs/rehype": "^1.22.2",
"@types/hast": "^3.0.4",
"@types/unist": "^3.0.3",
"bits-ui": "next",
"date-fns": "^4.1.0",
"gunzip-maybe": "^1.4.2",
"hast-util-heading": "^3.0.0",
"hast-util-heading-rank": "^3.0.0",
"hast-util-to-text": "^4.0.2",
"lucide-svelte": "^0.446.0",
"rehype-infer-description-meta": "^2.0.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-slug": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-frontmatter": "^5.0.0",
"remark-gemoji": "^8.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"shiki": "^1.22.2",
"tar-stream": "^3.1.7",
"unified": "^11.0.5",
"unist-util-map": "^4.0.0",
"vfile": "^6.0.3"
},
"patchedDependencies": {
"@shikijs/rehype@1.22.0": "patches/@shikijs%2Frehype@1.22.0.patch"
}
}

View file

@ -0,0 +1,26 @@
@shikis/rehype doesn't use the `fallbackLanguage` if `lazy` is used.
--- a/dist/core.mjs
+++ b/dist/core.mjs
@@ -1,6 +1,8 @@
import { visit } from 'unist-util-visit';
import { toString } from 'hast-util-to-string';
+import { bundledLanguages } from 'shiki';
+
const InlineCodeHandlers = {
"tailing-curly-colon": (_tree, node) => {
const raw = toString(node);
@@ -90,8 +92,12 @@ function rehypeShikiFromHighlighter(highlighter, options) {
const languageQueue = [];
const queue = [];
function getLanguage(lang) {
if (!lang)
return defaultLanguage;
+
+ if (!(lang in bundledLanguages))
+ return fallbackLanguage;
+
if (highlighter.getLoadedLanguages().includes(lang))
return lang;
if (lazy) {

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

92
website/src/app.css Normal file
View file

@ -0,0 +1,92 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
:root {
--color-background: 255 245 230;
--color-card: 245 230 210;
--color-card-hover: 240 225 205;
--color-border: 200 180 160;
--color-header: 250 234 215;
--color-body: 84 70 50;
--color-heading: 70 55 35;
--color-light: 0 0 0;
--color-input-bg: 245 230 210;
--color-input-border: 180 160 140;
--color-placeholder: 130 90 40;
--color-primary: 120 70 10;
--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) {
:root {
--color-background: 10 7 4;
--color-card: 28 22 17;
--color-card-hover: 40 32 25;
--color-border: 28 22 17;
--color-header: 20 16 12;
--color-body: 198 167 140;
--color-heading: 227 213 200;
--color-light: 255 255 255;
--color-input-bg: 20 13 8;
--color-input-border: 78 60 40;
--color-placeholder: 169 147 128;
--color-primary: 241 157 30;
--color-primary-hover: 255 172 42;
--color-primary-bg: 241 157 30;
--color-primary-fg: 10 7 4;
}
}
html {
scroll-padding-top: theme(spacing.24);
color-scheme: light dark;
}
body {
background-color: theme(colors.background);
color: theme(colors.body);
}
@keyframes cursor-blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}

13
website/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {}

12
website/src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,11 @@
<script lang="ts">
const props = $props()
</script>
<svg {...props} role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>GitHub</title>
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
fill="currentColor"
/>
</svg>

View file

@ -0,0 +1,23 @@
<svg {...$$restProps} viewBox="0 0 56 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>pesde</title>
<path
d="M0 28V26.3156H2.25652V12.2361H0.0635639V10.5517H4.44947L4.48125 11.9819L3.78205 12.3315C4.41769 11.6746 5.16986 11.1661 6.03857 10.8059C6.92846 10.4245 7.82895 10.2338 8.74003 10.2338C9.863 10.2338 10.88 10.4775 11.7911 10.9648C12.7234 11.4522 13.4544 12.1726 13.9841 13.126C14.5349 14.0795 14.8104 15.2448 14.8104 16.6221C14.8104 18.0416 14.5138 19.26 13.9205 20.277C13.3272 21.2728 12.5327 22.0356 11.5368 22.5653C10.5622 23.095 9.5028 23.3598 8.35865 23.3598C7.72301 23.3598 7.11916 23.2751 6.54708 23.1056C5.99619 22.9361 5.50887 22.7242 5.08511 22.4699C4.66135 22.1945 4.34353 21.8873 4.13165 21.5483L4.60838 21.4529L4.5766 26.3156H7.02381V28H0ZM7.94549 21.6118C9.19558 21.6118 10.2444 21.2092 11.0919 20.4041C11.9394 19.5778 12.3632 18.3807 12.3632 16.8127C12.3632 15.2872 11.9606 14.1113 11.1555 13.2849C10.3503 12.4586 9.3333 12.0454 8.1044 12.0454C7.72301 12.0454 7.26747 12.1196 6.73777 12.2679C6.20807 12.395 5.67837 12.6069 5.14867 12.9035C4.61898 13.2002 4.17403 13.5922 3.81383 14.0795L4.5766 12.7446L4.60838 20.7219L3.8774 19.7367C4.42828 20.3299 5.06392 20.7961 5.78431 21.1351C6.5047 21.4529 7.2251 21.6118 7.94549 21.6118Z"
fill="currentColor"
/>
<path
d="M18.37 17.7448C17.0563 17.7448 15.8592 17.4694 14.7786 16.9185C13.7192 16.3676 12.8717 15.5942 12.236 14.5984C11.6216 13.6026 11.3144 12.4478 11.3144 11.1341C11.3144 9.77811 11.611 8.61277 12.2043 7.63813C12.7975 6.64229 13.5921 5.87953 14.5879 5.34983C15.5837 4.79894 16.6961 4.5235 17.925 4.5235C19.1963 4.5235 20.2875 4.78835 21.1986 5.31805C22.1308 5.84775 22.8406 6.59992 23.3279 7.57456C23.8153 8.54921 24.0589 9.69336 24.0589 11.007V11.6109H13.3802L13.412 10.1489H21.7388C21.7388 9.32257 21.5693 8.62337 21.2303 8.05129C20.9125 7.45803 20.4676 7.01308 19.8955 6.71645C19.3234 6.39863 18.6666 6.23972 17.925 6.23972C17.1411 6.23972 16.4313 6.43042 15.7956 6.8118C15.16 7.17199 14.6515 7.70169 14.2701 8.4009C13.9099 9.07891 13.7298 9.90524 13.7298 10.8799C13.7298 11.9181 13.9205 12.8186 14.3019 13.5814C14.6833 14.3229 15.213 14.9056 15.891 15.3294C16.5902 15.732 17.3847 15.9332 18.2746 15.9332C19.2281 15.9332 20.1074 15.7425 20.9125 15.3612C21.7177 14.9798 22.4169 14.503 23.0101 13.931L23.8365 15.3612C23.2644 16.018 22.5228 16.5795 21.6117 17.0456C20.7006 17.5117 19.6201 17.7448 18.37 17.7448Z"
fill="currentColor"
/>
<path
d="M28.7199 22.6288C28.0631 22.6288 27.4275 22.5441 26.813 22.3746C26.2198 22.2051 25.7007 21.972 25.2557 21.6754C24.8108 21.3788 24.4718 21.0292 24.2387 20.6266L24.7154 20.9126V22.311H22.8721V17.9887H24.5247L24.7154 20.0545L24.2705 18.5608C24.5035 19.3447 25.0227 19.9486 25.8278 20.3723C26.6541 20.7961 27.5122 21.008 28.4021 21.008C29.2073 21.008 29.8747 20.8491 30.4044 20.5312C30.9553 20.2134 31.2307 19.7685 31.2307 19.1964C31.2307 18.5184 30.9977 18.0522 30.5315 17.798C30.0866 17.5225 29.3662 17.2789 28.3703 17.067L26.6223 16.6856C25.457 16.389 24.5353 15.997 23.8573 15.5097C23.1793 15.0012 22.8403 14.1642 22.8403 12.9989C22.8403 11.8759 23.2746 11.0178 24.1434 10.4245C25.0332 9.81009 26.135 9.50286 27.4487 9.50286C27.9572 9.50286 28.4869 9.57702 29.0378 9.72534C29.6098 9.85246 30.129 10.0538 30.5951 10.3292C31.0612 10.6046 31.3896 10.9436 31.5803 11.3462L31.1036 11.1873V9.75712H32.9787V14.0477H31.3261L30.9129 11.0284L31.4532 13.126C31.3684 12.7446 31.1248 12.4162 30.7222 12.1408C30.3408 11.8441 29.8853 11.6111 29.3556 11.4416C28.8471 11.2721 28.3386 11.1873 27.8301 11.1873C27.152 11.1873 26.5376 11.325 25.9867 11.6005C25.457 11.8759 25.1922 12.3209 25.1922 12.9353C25.1922 13.4015 25.3723 13.7617 25.7325 14.0159C26.1138 14.2702 26.7283 14.5033 27.5758 14.7151L29.1967 15.0647C30.0018 15.2554 30.7222 15.4885 31.3579 15.7639C32.0147 16.0182 32.5338 16.3996 32.9152 16.9081C33.2966 17.3954 33.4872 18.0946 33.4872 19.0057C33.4872 19.832 33.2542 20.5206 32.788 21.0715C32.3431 21.6012 31.7498 21.9932 31.0083 22.2475C30.2879 22.5017 29.5251 22.6288 28.7199 22.6288Z"
fill="currentColor"
/>
<path
d="M37.1104 18.5607C35.9662 18.5607 34.9068 18.3064 33.9322 17.7979C32.9787 17.2682 32.2054 16.5054 31.6121 15.5096C31.0188 14.5138 30.7222 13.3272 30.7222 11.95C30.7222 10.5304 30.9977 9.34389 31.5485 8.39043C32.1206 7.41579 32.8728 6.67421 33.8051 6.1657C34.7373 5.65719 35.7544 5.40293 36.8561 5.40293C37.746 5.40293 38.6253 5.57243 39.494 5.91144C40.3839 6.22926 41.1679 6.6848 41.8459 7.27807L41.0831 7.5641V1.65266H38.8584V0H43.435V16.5266H45.4055V18.2111H41.0831V16.5584L41.8141 16.6855C41.1997 17.2788 40.5005 17.7449 39.7165 18.0839C38.9537 18.4018 38.085 18.5607 37.1104 18.5607ZM37.6189 16.7809C38.4452 16.7809 39.208 16.622 39.9072 16.3042C40.6276 15.9863 41.2844 15.5626 41.8777 15.0329L41.0831 16.1135V7.85014L41.8777 8.99429C41.2208 8.42221 40.4793 7.97727 39.6529 7.65945C38.8478 7.34163 38.0744 7.18272 37.3329 7.18272C36.5277 7.18272 35.8073 7.37341 35.1717 7.75479C34.5572 8.13618 34.0699 8.69766 33.7097 9.43924C33.3495 10.1596 33.1694 11.0601 33.1694 12.1407C33.1694 13.1366 33.3707 13.9841 33.7733 14.6833C34.1759 15.3613 34.7056 15.8804 35.3624 16.2406C36.0404 16.6008 36.7926 16.7809 37.6189 16.7809Z"
fill="currentColor"
/>
<path
d="M50.3188 24.2004C49.0051 24.2004 47.808 23.925 46.7274 23.3741C45.668 22.8232 44.8205 22.0498 44.1848 21.054C43.5704 20.0582 43.2632 18.9034 43.2632 17.5898C43.2632 16.2337 43.5598 15.0684 44.1531 14.0937C44.7463 13.0979 45.5409 12.3351 46.5367 11.8054C47.5325 11.2545 48.6449 10.9791 49.8738 10.9791C51.1451 10.9791 52.2363 11.2439 53.1474 11.7736C54.0796 12.3033 54.7894 13.0555 55.2767 14.0302C55.7641 15.0048 56.0077 16.149 56.0077 17.4626V18.0665H45.329L45.3608 16.6045H53.6876C53.6876 15.7782 53.5181 15.079 53.1791 14.5069C52.8613 13.9136 52.4164 13.4687 51.8443 13.172C51.2722 12.8542 50.6154 12.6953 49.8738 12.6953C49.0899 12.6953 48.3801 12.886 47.7444 13.2674C47.1088 13.6276 46.6003 14.1573 46.2189 14.8565C45.8587 15.5345 45.6786 16.3609 45.6786 17.3355C45.6786 18.3737 45.8693 19.2742 46.2507 20.037C46.6321 20.7786 47.1617 21.3612 47.8398 21.785C48.539 22.1876 49.3335 22.3888 50.2234 22.3888C51.1769 22.3888 52.0562 22.1982 52.8613 21.8168C53.6665 21.4354 54.3657 20.9587 54.9589 20.3866L55.7853 21.8168C55.2132 22.4736 54.4716 23.0351 53.5605 23.5012C52.6494 23.9673 51.5688 24.2004 50.3188 24.2004Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,9 @@
<svg {...$$restProps} viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>pesde</title>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M49.6025 0L92.9038 25V75L49.6025 100L6.30127 75V25L49.6025 0ZM14.3013 29.6188L49.6025 9.2376L84.9038 29.6188V70.3812L49.6025 90.7624L33.6148 81.5319V67.3848C34.5167 68.5071 35.6388 69.4215 36.981 70.1279C38.9701 71.148 41.0357 71.658 43.1779 71.658C46.442 71.658 49.1452 70.8929 51.2873 69.3629C53.4805 67.7818 55.1126 65.7672 56.1836 63.319C57.0915 61.3382 57.632 59.274 57.8054 57.1263C59.8723 57.7457 62.2157 58.0554 64.8356 58.0554C67.6918 58.0554 70.3695 57.6473 72.8686 56.8313C75.3678 55.9642 77.4079 54.8167 78.989 53.3886L75.7758 47.8038C74.5517 48.9258 72.9961 49.8439 71.109 50.5579C69.2219 51.221 67.2073 51.5525 65.0652 51.5525C61.3929 51.5525 58.6643 50.6854 56.8792 48.9513C56.7195 48.7962 56.567 48.6365 56.4217 48.472C55.6102 47.5539 55.0211 46.4896 54.6546 45.2791L54.6443 45.2452L54.669 45.2791H79.2185V41.9894C79.2185 39.0313 78.5555 36.3536 77.2294 33.9565C75.9543 31.5593 74.0927 29.6467 71.6445 28.2186C69.2474 26.7395 66.3657 26 62.9995 26C59.6843 26 56.8027 26.7395 54.3545 28.2186C51.9064 29.6467 50.0193 31.5593 48.6932 33.9565C47.6743 35.7983 47.0469 37.8057 46.8108 39.9788C45.6888 39.728 44.4778 39.6026 43.1779 39.6026C41.0357 39.6026 38.9701 40.1127 36.981 41.1327C35.3162 41.9651 33.9902 43.1549 33.0028 44.7023V40.3677H20.6855V46.2585H25.8113V77.0266L14.3013 70.3812V29.6188ZM55.1961 36.0986C54.6528 37.1015 54.3321 38.1216 54.234 39.1588H71.7976C71.7976 38.0367 71.4405 36.9401 70.7265 35.8691C70.0634 34.747 69.0689 33.8035 67.7428 33.0384C66.4677 32.2734 64.8867 31.8908 62.9995 31.8908C61.1124 31.8908 59.5058 32.2989 58.1798 33.1149C56.9047 33.88 55.9101 34.8745 55.1961 36.0986ZM49.6451 51.5692C49.3076 50.6641 48.8381 49.871 48.2367 49.1898C48.0885 49.0219 47.9323 48.8609 47.7681 48.7067C46.085 47.0746 44.0449 46.2585 41.6478 46.2585C40.1177 46.2585 38.6131 46.5645 37.134 47.1766C35.8594 47.6773 34.6863 48.5438 33.6148 49.7759V61.47C34.6863 62.6664 35.8594 63.5378 37.134 64.084C38.6131 64.6961 40.1177 65.0021 41.6478 65.0021C44.0449 65.0021 46.085 64.1861 47.7681 62.554C49.4512 60.9219 50.2928 58.6012 50.2928 55.5921C50.2928 54.0679 50.0769 52.727 49.6451 51.5692Z"
fill="currentColor"
></path>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,74 @@
<script lang="ts">
import { Select, type SelectSingleRootProps, type WithoutChildren } from "bits-ui"
import { Check, ChevronsUpDown } from "lucide-svelte"
import type { Snippet } from "svelte"
type Props = Omit<WithoutChildren<SelectSingleRootProps>, "type"> & {
placeholder?: string
items: { value: string; label: string; disabled?: boolean }[]
contentProps?: WithoutChildren<Select.ContentProps>
contentClass?: string
triggerClass?: string
trigger?: Snippet<[Record<string, unknown>, string]>
sameWidth?: boolean
}
let {
value = $bindable(""),
items,
trigger,
contentProps,
contentClass = "",
triggerClass = "",
placeholder,
sameWidth = true,
id,
open = $bindable(false),
...restProps
}: Props = $props()
const selectedLabel = $derived(items.find((item) => item.value === value)?.label)
const triggerLabel = $derived(selectedLabel ?? placeholder ?? "")
</script>
<Select.Root type="single" bind:value bind:open {...restProps}>
<Select.Trigger {id}>
{#snippet child({ props })}
{#if trigger}
{@render trigger(props, triggerLabel)}
{:else}
<button
{...props}
class={`border-input-border bg-input-bg ring-primary-bg/20 focus:border-primary relative flex h-11 w-full items-center rounded border px-4 outline-none ring-0 transition focus:ring-4 data-[disabled]:opacity-50 ${triggerClass}`}
>
{triggerLabel}
<ChevronsUpDown class="ml-auto size-5" />
</button>
{/if}
{/snippet}
</Select.Trigger>
<Select.Portal>
<Select.Content
sideOffset={8}
collisionPadding={8}
{...contentProps}
class={`bg-card border-input-border z-50 max-h-[var(--bits-floating-available-height)] origin-top overflow-y-auto rounded-lg border p-1 shadow-lg ${sameWidth ? "w-[var(--bits-select-anchor-width)]" : ""} ${contentClass}`}
>
{#each items as { value, label, disabled } (value)}
<Select.Item
{value}
{label}
{disabled}
class="data-[highlighted]:bg-card-hover flex h-10 flex-shrink-0 items-center truncate rounded-sm px-3"
>
{#snippet children({ selected })}
{label}
{#if selected}
<Check class="ml-auto size-4" />
{/if}
{/snippet}
</Select.Item>
{/each}
</Select.Content>
</Select.Portal>
</Select.Root>

116
website/src/lib/markdown.ts Normal file
View file

@ -0,0 +1,116 @@
import rehypeShikiFromHighlighter from "@shikijs/rehype/core"
import type { Nodes } from "hast"
import { heading } from "hast-util-heading"
import { headingRank } from "hast-util-heading-rank"
import { toText } from "hast-util-to-text"
import rehypeInferDescriptionMeta from "rehype-infer-description-meta"
import rehypeRaw from "rehype-raw"
import rehypeSanitize from "rehype-sanitize"
import rehypeSlug from "rehype-slug"
import rehypeStringify from "rehype-stringify"
import remarkFrontmatter from "remark-frontmatter"
import remarkGemoji from "remark-gemoji"
import remarkGfm from "remark-gfm"
import remarkParse from "remark-parse"
import remarkRehype from "remark-rehype"
import { createCssVariablesTheme, createHighlighter } from "shiki"
import { unified } from "unified"
import type { Node } from "unist"
import { map } from "unist-util-map"
const highlighter = createHighlighter({
themes: [],
langs: [],
})
export const markdown = (async () => {
return unified()
.use(remarkParse)
.use(remarkFrontmatter)
.use(remarkGfm)
.use(remarkGemoji)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSanitize)
.use(rehypeShikiFromHighlighter, await highlighter, {
lazy: true,
theme: createCssVariablesTheme({
name: "css-variables",
variablePrefix: "--shiki-",
variableDefaults: {},
fontStyle: true,
}),
fallbackLanguage: "text",
})
.use(rehypeStringify)
.freeze()
})()
export type TocItem = {
id: string
title: string
level: number
}
export const docsMarkdown = (async () => {
return unified()
.use(remarkParse)
.use(remarkFrontmatter)
.use(remarkGfm)
.use(remarkGemoji)
.use(remarkRehype, { allowDangerousHtml: true, clobberPrefix: "" })
.use(rehypeSlug)
.use(() => (node, file) => {
const toc: TocItem[] = []
file.data.toc = toc
return map(node as Nodes, (node) => {
if (node.type === "element" && node.tagName === "a") {
const fullUrl = new URL(node.properties.href as string, `file://${file.path}`)
let href = node.properties.href as string
if (fullUrl.protocol === "file:") {
href = file.data.basePath + fullUrl.pathname.replace(/\.mdx?$/, "") + fullUrl.hash
}
return {
...node,
properties: {
...node.properties,
href,
},
}
}
if (heading(node)) {
const rank = headingRank(node)
if (rank && typeof node.properties.id === "string" && rank >= 2 && rank <= 3) {
toc.push({
id: node.properties.id,
title: toText(node),
level: rank,
})
}
}
return node
}) as Node
})
.use(rehypeRaw)
.use(rehypeSanitize)
.use(rehypeShikiFromHighlighter, await highlighter, {
lazy: true,
theme: createCssVariablesTheme({
name: "css-variables",
variablePrefix: "--shiki-",
variableDefaults: {},
fontStyle: true,
}),
fallbackLanguage: "text",
})
.use(rehypeInferDescriptionMeta, {
selector: "p",
})
.use(rehypeStringify)
.freeze()
})()

View file

@ -0,0 +1,106 @@
import { PUBLIC_REGISTRY_URL } from "$env/static/public"
export type SearchResponse = {
count: number
data: PackageResponse[]
}
export type PackageVersionsResponse = PackageResponse[]
export type PackageVersionResponse = PackageResponse
export type PackageResponse = {
name: string
version: string
targets: TargetInfo[]
description: string
published_at: string
license?: string
authors?: string[]
repository?: string
dependencies: Record<string, DependencyEntry>
docs?: DocEntry[]
}
export type TargetInfo = {
kind: TargetKind
lib: boolean
bin: boolean
}
export type TargetKind = "roblox" | "roblox_server" | "lune" | "luau"
export type DependencyEntry = [DependencyInfo, DependencyKind]
export type DependencyInfo =
| {
index: string
name: string
target?: string
version: string
}
| {
index: string
wally: string
version: string
}
export type DependencyKind = "standard" | "peer" | "dev"
export type DocEntry = DocEntryCategory | DocEntryPage
export type DocEntryBase = {
label: string
position: number
}
export type DocEntryCategory = DocEntryBase & {
items?: DocEntry[]
collapsed?: boolean
}
export type DocEntryPage = DocEntryBase & {
name: string
hash: string
}
export const TARGET_KIND_DISPLAY_NAMES: Record<TargetKind, string> = {
roblox: "Roblox",
roblox_server: "Roblox (server)",
lune: "Lune",
luau: "Luau",
}
export const DEPENDENCY_KIND_DISPLAY_NAMES: Record<DependencyKind, string> = {
standard: "Dependencies",
peer: "Peer Dependencies",
dev: "Dev Dependencies",
}
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 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

@ -0,0 +1,8 @@
<script>
import { page } from "$app/stores"
</script>
<div class="mx-auto max-w-screen-xl px-4 py-32 text-center">
<h1 class="mb-1 text-4xl font-bold text-heading">{$page.status}</h1>
<p class="text-lg">{$page.error?.message}</p>
</div>

View file

@ -0,0 +1,14 @@
<script>
import Footer from "./Footer.svelte"
import Header from "./Header.svelte"
const { children } = $props()
</script>
<Header />
<main class="min-h-screen">
{@render children()}
</main>
<Footer />

View file

@ -0,0 +1,56 @@
<script lang="ts">
import { formatDistanceToNow } from "date-fns"
import { TARGET_KIND_DISPLAY_NAMES } from "$lib/registry-api"
import Hero from "./Hero.svelte"
const { data } = $props()
let displayDates = $state(false)
$effect(() => {
displayDates = true
})
</script>
<Hero />
<section class="mx-auto max-w-screen-lg px-4 pb-32">
<h2 class="text-heading mb-4 text-2xl font-semibold">
<a id="recently-published" href="#recently-published">Recently Published</a>
</h2>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{#each data.packages.slice(0, 24) as pkg}
{@const [scope, name] = pkg.name.split("/")}
<article
class="bg-card hover:bg-card-hover relative overflow-hidden rounded px-5 py-4 transition"
>
<h3 class="truncate text-xl font-semibold">
<a href={`/packages/${pkg.name}`} class="after:absolute after:inset-0 after:content-['']">
<span class="text-heading">{scope}/</span><span class="text-light">{name}</span>
</a>
</h3>
<div class="text-primary mb-3 flex overflow-hidden whitespace-nowrap text-sm font-semibold">
<span class="truncate">v{pkg.version}</span>
<span class="whitespace-pre"
>{` · ${pkg.targets
.map((target) => TARGET_KIND_DISPLAY_NAMES[target.kind])
.join(", ")}`}</span
>
</div>
<p class="mb-3 line-clamp-2 h-[2lh] overflow-hidden text-sm">{pkg.description}</p>
<div class="text-heading text-sm font-semibold">
<time datetime={pkg.published_at} class:invisible={!displayDates}>
{#if displayDates}
{formatDistanceToNow(new Date(pkg.published_at), { addSuffix: true })}
{:else}
...
{/if}
</time>
</div>
</article>
{/each}
</div>
</section>

View file

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

View file

@ -0,0 +1,17 @@
<footer class="mx-auto max-w-screen-lg px-4">
<div class="border-t py-16 text-sm">
<p>Licensed under the MIT License.</p>
<p class="mb-4">Copyright © 2024 daimond113</p>
<p>
Designed with ♥︎ by
<a
href="https://github.com/lukadev-0"
class="font-semibold underline underline-offset-2"
target="_blank"
rel="noreferrer noopener"
>
lukadev
</a>.
</p>
</div>
</footer>

View file

@ -0,0 +1,77 @@
<script lang="ts">
import { navigating } from "$app/stores"
import GitHub from "$lib/components/GitHub.svelte"
import Logo from "$lib/components/Logo.svelte"
import { Dialog } from "bits-ui"
import { Menu, X } from "lucide-svelte"
import { fade, fly } from "svelte/transition"
import Search from "./Search.svelte"
let dialogOpen = $state(false)
$effect(() => {
if ($navigating) {
dialogOpen = false
}
})
</script>
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Trigger>
<span class="sr-only">open menu</span>
<Menu aria-hidden="true" />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content forceMount>
{#snippet child({ props, open })}
{#if open}
<div {...props} class="fixed inset-0 top-0 z-50 flex flex-col">
<Dialog.Title class="sr-only">Menu</Dialog.Title>
<div transition:fade={{ duration: 200 }} class="bg-header">
<div class="relative z-50 flex h-14 flex-shrink-0 items-center justify-between px-4">
<a href="/">
<Logo class="text-primary h-7" />
</a>
<Dialog.Close>
<span class="sr-only">close menu</span>
<X aria-hidden="true" />
</Dialog.Close>
</div>
<div class="px-4 py-1">
<Search />
</div>
</div>
<div
class="bg-header flex flex-grow flex-col overflow-hidden"
transition:fade={{ duration: 200 }}
>
<nav
class="flex h-full flex-col px-4 pt-2"
transition:fly={{ y: "-2%", duration: 200 }}
>
<div class="flex flex-grow flex-col border-y py-3">
{#snippet item(href: string, text: string)}
<a {href} class="hover:bg-card/80 flex h-10 items-center rounded px-3">{text}</a
>
{/snippet}
{@render item("https://docs.pesde.daimond113.com/", "Documentation")}
{@render item("https://docs.pesde.daimond113.com/registry/policies", "Policies")}
</div>
<div class="flex items-center py-5">
<a
href="https://github.com/daimond113/pesde"
target="_blank"
rel="noreferrer noopener"
>
<GitHub class="size-6" />
</a>
</div>
</nav>
</div>
</div>
{/if}
{/snippet}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>

View file

@ -0,0 +1,81 @@
<script lang="ts">
import GitHub from "$lib/components/GitHub.svelte"
import Logo from "$lib/components/Logo.svelte"
import type { Action } from "svelte/action"
import Hamburger from "./Hamburger.svelte"
import Search from "./Search.svelte"
let hideSearch = $state(false)
let headerHidden = $state(false)
const handleScroll = () => {
hideSearch = window.scrollY > 0
}
$effect(() => {
handleScroll()
})
const headerIntersection: Action = (node) => {
$effect(() => {
const callback: IntersectionObserverCallback = (entries) => {
for (const entry of entries) {
headerHidden = !entry.isIntersecting
}
}
const observer = new IntersectionObserver(callback, {
threshold: 0,
rootMargin: "-57px 0px 0px 0px",
})
observer.observe(node)
return () => {
observer.disconnect()
}
})
}
</script>
<svelte:window on:scroll={handleScroll} />
<header
use:headerIntersection
data-hidden={headerHidden ? true : null}
data-hide-search={hideSearch ? true : null}
class="bg-header group border-b pt-14 sm:!bg-transparent"
>
<div
class="bg-header fixed inset-x-0 top-0 z-50 bg-opacity-100 backdrop-blur-lg transition-[background] group-data-[hidden]:border-b group-data-[hidden]:bg-opacity-80 sm:!border-b sm:!bg-opacity-80"
>
<div class="mx-auto flex h-14 max-w-screen-lg items-center justify-between px-4">
<a href="/">
<Logo class="text-primary h-7" />
</a>
<div class="hidden w-full max-w-80 sm:flex">
<Search />
</div>
<div class="[&_a:hover]:text-heading hidden items-center space-x-6 sm:flex [&_a]:transition">
<nav class="flex items-center space-x-6 font-medium">
<a href="https://docs.pesde.daimond113.com/">Docs</a>
<a href="https://docs.pesde.daimond113.com/registry/policies">Policies</a>
</nav>
<a href="https://github.com/daimond113/pesde" target="_blank" rel="noreferrer noopener">
<GitHub class="size-6" />
</a>
</div>
<div class="flex items-center sm:hidden">
<Hamburger />
</div>
</div>
</div>
<div class="overflow-hidden px-4 pb-2 pt-1 sm:hidden">
<div
class="transition duration-300 group-data-[hide-search]:-translate-y-1 group-data-[hide-search]:opacity-0"
>
<Search />
</div>
</div>
</header>

View file

@ -0,0 +1,90 @@
<script lang="ts">
import { onMount } from "svelte"
export const prerender = true
const tools = ["Luau", "Roblox", "Lune"]
let typewriteText = $state("Luau")
let blink = $state(true)
let cursorVisible = $state(false)
onMount(() => {
let current = 0
let timeout: NodeJS.Timeout
function typewrite(text: string) {
blink = false
let progress = 0
timeout = setInterval(() => {
progress++
typewriteText = text.slice(0, progress)
if (progress >= text.length) {
blink = true
clearInterval(timeout)
timeout = setTimeout(() => clear(), 3500)
}
}, 120)
}
function clear() {
blink = false
let progress = typewriteText.length
timeout = setInterval(() => {
progress--
typewriteText = typewriteText.slice(0, progress)
if (progress <= 0) {
clearInterval(timeout)
timeout = setTimeout(() => {
current++
if (current >= tools.length) current = 0
typewrite(tools[current])
}, 1000)
}
}, 80)
}
cursorVisible = true
timeout = setTimeout(() => {
clear()
}, 4500)
return () => {
clearTimeout(timeout)
}
})
</script>
<section class="mx-auto max-w-screen-lg px-4 py-32">
<h1 class="text-heading mb-3 text-2xl font-semibold md:mb-6 md:text-4xl lg:text-5xl">
Manage your packages<br />
<span class="sr-only"> for Luau</span>
<span class="text-primary" aria-hidden="true">
for {typewriteText}{#if cursorVisible}
<span
class="ml-1 inline-block h-[1.125rem] w-0.5 bg-current duration-100 md:h-7 lg:h-9"
class:animate-cursor-blink={blink}
></span>
{/if}
</span>
</h1>
<p class="mb-5 max-w-sm md:mb-8 md:max-w-md md:text-lg">
A package manager for the Luau programming language, supporting multiple runtimes including
Roblox and Lune.
</p>
<a
href="https://docs.pesde.daimond113.com/"
class="bg-primary-bg text-primary-fg hover:bg-primary-hover inline-flex h-10 items-center rounded px-4 font-semibold transition md:h-11 md:px-5"
>
Get Started
</a>
</section>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Search } from "lucide-svelte"
import { searchQuery } from "./search-state.svelte"
</script>
<form action="/search" class="w-full">
<label
class="relative flex h-9 w-full items-center overflow-hidden rounded border border-input-border bg-input-bg px-2 outline-none ring-0 ring-primary-bg/20 transition focus-within:border-primary focus-within:ring-4"
>
<Search class="size-5" aria-label="search" />
<input
name="q"
placeholder="search packages..."
class="absolute inset-0 bg-transparent pl-9 pr-2 text-heading placeholder:text-placeholder"
bind:value={searchQuery.value}
/>
</label>
</form>

View file

@ -0,0 +1,6 @@
import { error } from "@sveltejs/kit"
import type { PageLoad } from "./$types"
export const load: PageLoad = () => {
error(404, "Not Found")
}

View file

@ -0,0 +1,91 @@
<script lang="ts">
import { page } from "$app/stores"
import { formatDistanceToNow } from "date-fns"
import { onMount, setContext, untrack } from "svelte"
import Tab from "./Tab.svelte"
import TargetSelector from "./TargetSelector.svelte"
let { children, data } = $props()
const [scope, name] = $derived(data.pkg.name.split("/"))
let currentPkg = $state(data.pkg)
let currentTarget = $state(
data.pkg.targets.find((target) => target.kind === $page.params.target) ?? data.pkg.targets[0],
)
setContext("currentPkg", {
get value() {
return currentPkg
},
set value(v) {
currentPkg = v
},
})
setContext("currentTarget", {
get value() {
return currentTarget
},
set value(v) {
currentTarget = v
},
})
const getReadme = () => {
if ("target" in $page.params) {
return `${$page.params.version}/${$page.params.target}`
}
return ""
}
const pkgVersion = $derived(currentPkg.version)
const pkgDescription = $derived(currentPkg.description)
let pkgDate = $state<null | string>(null)
let readme = $state(getReadme())
$effect(() => {
pkgDate = formatDistanceToNow(new Date(currentPkg.published_at), { addSuffix: true })
readme = untrack(getReadme)
})
onMount(() => {
return page.subscribe((page) => {
if (pkgDate === null || page.params.target !== undefined) {
currentTarget =
data.pkg.targets.find((target) => target.kind === page.params.target) ??
data.pkg.targets[0]
currentPkg = data.pkg
}
})
})
</script>
<div class="mx-auto max-w-prose px-4 py-16 lg:max-w-screen-lg">
<h1 class="text-3xl font-bold">
<span class="text-heading">{scope}/</span><span class="text-light">{name}</span>
</h1>
<div class="text-primary mb-2 font-semibold" class:invisible={pkgDate === null}>
v{pkgVersion} ·
<time datetime={data.pkg.published_at} title={new Date(data.pkg.published_at).toLocaleString()}>
published {pkgDate ?? "..."}
</time>
</div>
<p class="mb-6 max-w-prose">{pkgDescription}</p>
<div class="mb-8 lg:hidden">
<TargetSelector />
</div>
<nav class="flex w-full flex-col sm:flex-row sm:border-b-2">
<Tab tab={readme}>Readme</Tab>
<Tab tab={`${pkgVersion}/${currentTarget.kind}/dependencies`}>Dependencies</Tab>
<Tab tab="versions">Versions</Tab>
{#if currentPkg.docs && currentPkg.docs.length > 0}
<Tab tab={`${pkgVersion}/${currentTarget.kind}/docs`}>Documentation</Tab>
{/if}
</nav>
{@render children()}
</div>

View file

@ -0,0 +1,46 @@
import {
fetchRegistryJson,
RegistryHttpError,
type PackageVersionResponse,
} from "$lib/registry-api"
import { error } from "@sveltejs/kit"
import type { LayoutLoad } from "./$types"
type FetchPackageOptions = {
scope: string
name: string
version?: string
target?: string
}
const fetchPackage = async (fetcher: typeof fetch, options: FetchPackageOptions) => {
const { scope, name, version = "latest", target = "any" } = options
try {
return await fetchRegistryJson<PackageVersionResponse>(
`packages/${encodeURIComponent(`${scope}/${name}`)}/${version}/${target}`,
fetcher,
)
} catch (e) {
if (e instanceof RegistryHttpError && e.response.status === 404) {
error(404, "This package does not exist.")
}
throw e
}
}
export const load: LayoutLoad = async ({ params, fetch }) => {
const { scope, name, version, target } = params
if (version !== undefined && target === undefined) {
error(404, "Not Found")
}
const options = version !== undefined ? { scope, name, version, target } : { scope, name }
const pkg = await fetchPackage(fetch, options)
return {
pkg,
}
}

View file

@ -0,0 +1,33 @@
<script lang="ts">
import { page } from "$app/stores"
import type { Snippet } from "svelte"
type Props = {
tab: string
children: Snippet
}
const { tab, children }: Props = $props()
const basePath = $derived.by(() => {
const { scope, name } = $page.params
return `/packages/${scope}/${name}`
})
const activeTab = $derived(
$page.url.pathname.slice(basePath.length).replace(/^\//, "").replace(/\/$/, ""),
)
const href = $derived(`${basePath}/${tab}`)
const active = $derived(activeTab === tab)
const linkClass = $derived(
"font-semibold px-5 inline-flex items-center transition rounded-r h-12 border-l-2 " +
"sm:rounded-r-none sm:rounded-t sm:border-b-2 sm:border-l-0 sm:h-10 sm:-mb-0.5 " +
(active ? "text-primary border-primary bg-primary-bg/20" : "hover:bg-border/30"),
)
</script>
<a {href} class={linkClass}>
{@render children()}
</a>

View file

@ -0,0 +1,52 @@
<script lang="ts">
import { goto } from "$app/navigation"
import { page } from "$app/stores"
import Select from "$lib/components/Select.svelte"
import { TARGET_KIND_DISPLAY_NAMES, type TargetInfo, type TargetKind } from "$lib/registry-api"
import { Label, useId } from "bits-ui"
import { getContext } from "svelte"
const currentTarget = getContext<{ value: TargetInfo }>("currentTarget")
const basePath = $derived.by(() => {
const { scope, name } = $page.params
if ("target" in $page.params) {
const { version } = $page.params
return `/packages/${scope}/${name}/${version}`
}
return `/packages/${scope}/${name}/latest`
})
const items = ($page.data.pkg.targets as TargetInfo[]).map((target) => {
return {
value: target.kind,
label: TARGET_KIND_DISPLAY_NAMES[target.kind as TargetKind],
}
})
const id = useId()
let disabled = $state(false)
let open = $state(false)
</script>
<div class="text-heading mb-1 text-lg font-semibold">
<Label.Root for={id} onclick={() => (open = true)}>Target</Label.Root>
</div>
<Select
{items}
{disabled}
{id}
bind:open
name="target-selector"
allowDeselect={false}
value={currentTarget.value.kind}
triggerClass="mb-6"
onValueChange={(selected) => {
disabled = true
goto(`${basePath}/${selected}`).finally(() => {
disabled = false
})
}}
/>

View file

@ -0,0 +1,67 @@
import {
fetchRegistryJson,
RegistryHttpError,
type PackageVersionResponse,
type PackageVersionsResponse,
} from "$lib/registry-api"
import { error, redirect } from "@sveltejs/kit"
import type { LayoutLoad } from "./$types"
type FetchPackageOptions = {
scope: string
name: string
version: string
target: string
}
const fetchPackageAndVersions = async (fetcher: typeof fetch, options: FetchPackageOptions) => {
const { scope, name, version, target } = options
try {
const [pkg, versions] = await Promise.all([
fetchRegistryJson<PackageVersionResponse>(
`packages/${encodeURIComponent(`${scope}/${name}`)}/${version}/${target}`,
fetcher,
),
fetchRegistryJson<PackageVersionsResponse>(
`packages/${encodeURIComponent(`${scope}/${name}`)}`,
fetcher,
),
])
versions.reverse()
return { pkg, versions }
} catch (e) {
if (e instanceof RegistryHttpError && e.response.status === 404) {
error(404, "This package does not exist.")
}
throw e
}
}
export const load: LayoutLoad = async ({ params, url, fetch }) => {
const { scope, name, version, target } = params
if (version !== undefined && target === undefined) {
error(404, "Not Found")
}
if (version === undefined || target === undefined || version === "latest" || target === "any") {
const pkg = await fetchRegistryJson<PackageVersionResponse>(
`packages/${encodeURIComponent(`${scope}/${name}`)}/${version ?? "latest"}/${target ?? "any"}`,
fetch,
)
const path = url.pathname.split("/").slice(6).join("/")
return redirect(303, `/packages/${scope}/${name}/${pkg.version}/${pkg.targets[0].kind}/${path}`)
}
const { pkg, versions } = await fetchPackageAndVersions(fetch, { scope, name, version, target })
return {
pkg,
versions: versions.map((v) => v.version),
}
}

View file

@ -0,0 +1,169 @@
<script lang="ts">
import { page } from "$app/stores"
import Logo from "$lib/components/Logo.svelte"
import Logomark from "$lib/components/Logomark.svelte"
import type { TocItem } from "$lib/markdown"
import { ChevronsUpDown } from "lucide-svelte"
import type { Action } from "svelte/action"
import Hamburger from "./Hamburger.svelte"
import TocMobile from "./MobileNavbar.svelte"
import Sidebar from "./Sidebar.svelte"
import Tab from "./Tab.svelte"
import TargetSelector from "./TargetSelector.svelte"
import Toc from "./Toc.svelte"
import TocObserver from "./TocObserver.svelte"
import VersionSelector from "./VersionSelector.svelte"
const { children, data } = $props()
const [scope, name] = data.pkg.name.split("/")
let hideNavigation = $state(false)
let headerHidden = $state(false)
const handleScroll = () => {
hideNavigation = window.scrollY > 0
}
$effect(() => {
handleScroll()
})
const headerIntersection: Action = (node) => {
$effect(() => {
const callback: IntersectionObserverCallback = (entries) => {
for (const entry of entries) {
headerHidden = !entry.isIntersecting
}
}
const observer = new IntersectionObserver(callback, {
threshold: 0,
rootMargin: "-57px 0px 0px 0px",
})
observer.observe(node)
return () => {
observer.disconnect()
}
})
}
const toc: TocItem[] = $derived(
$page.error
? [
{
id: "_top",
title: "Overview",
level: 2,
},
]
: ($page.data.toc ?? []),
)
</script>
<svelte:window on:scroll={handleScroll} />
<TocObserver toc={$page.data.toc ?? []} />
<div class="min-h-screen">
<header
class="bg-header group w-full border-b pt-14"
use:headerIntersection
data-hide-navigation={hideNavigation ? true : null}
data-hidden={headerHidden ? true : null}
>
<div
class="bg-header fixed top-0 z-10 w-full backdrop-blur-lg transition-[background] group-data-[hidden]:border-b xl:group-data-[hidden]:bg-opacity-80"
>
<div class="mx-auto flex h-14 max-w-screen-2xl items-center px-4">
{#snippet separator()}
<span class="text-body/60 px-2 text-xl">/</span>
{/snippet}
<span class="flex min-w-0 items-center">
<a
class="flex min-w-0 items-center"
href={`/packages/${scope}/${name}/${$page.params.version ?? "latest"}/${$page.params.target ?? "any"}`}
>
<span class="text-primary mr-2">
<Logomark class="h-7" />
</span>
<span class="min-w-0 truncate">{scope}</span>
{@render separator()}
<span class="text-heading min-w-0 truncate font-medium">{name}</span>
</a>
<span class="hidden items-center lg:flex">
{#snippet trigger(props: Record<string, unknown>, label: string)}
<button
{...props}
class="flex items-center transition-opacity data-[disabled]:opacity-50"
>
{label}
<ChevronsUpDown class="ml-1 size-4" />
</button>
{/snippet}
{@render separator()}
<VersionSelector {trigger} sameWidth={false} />
{@render separator()}
<TargetSelector {trigger} sameWidth={false} />
</span>
</span>
<div class="ml-auto flex items-center lg:hidden">
<Hamburger />
</div>
</div>
</div>
<div class="group-data-[hidden]:invisible">
<div class="-mb-px overflow-hidden pb-px">
<nav
class="transition duration-300 group-data-[hide-navigation]:-translate-y-1 group-data-[hide-navigation]:opacity-0"
>
<div class="mx-auto flex max-w-screen-2xl px-4">
<Tab tab="docs" active={$page.data.activeTab == "docs"}>Docs</Tab>
<Tab tab="reference" active={$page.data.activeTab == "reference"}>Reference</Tab>
</div>
</nav>
</div>
</div>
</header>
<TocMobile {toc} />
<div class="mx-auto flex max-w-screen-2xl">
<Sidebar items={$page.data.sidebar ?? []} />
<main class="mx-auto w-full max-w-screen-md px-4 md:px-6">
<div id="_top"></div>
{@render children()}
</main>
<Toc {toc} />
</div>
</div>
<footer class="mx-auto max-w-screen-2xl px-4">
<div class="border-t py-16">
<p class="text-center font-semibold">
Documentation powered by<br />
<span class="mt-2 inline-block">
<a href="/"><Logo class="inline h-8" /></a>
<span class="mx-2 text-lg font-semibold">+</span>
<a href="https://eryn.io/moonwave">
<svg
class="inline h-6"
viewBox="0 0 76 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Moonwave</title>
<path
d="M0.212789 31.8083C-0.187211 31.6083 0.012789 31.0083 0.512789 31.1083C6.01279 32.7083 15.9128 33.8083 21.4128 24.5083C28.8128 12.1083 31.8128 -5.59168 58.0128 1.70832C58.5128 1.80832 58.5128 2.50832 58.1128 2.60832C54.5128 4.10832 45.3128 9.20832 49.9128 19.5083C49.9128 19.5083 53.3128 8.50832 66.0128 11.8083C66.3128 11.9083 66.3128 12.3083 66.0128 12.5083C63.5128 13.6083 56.5128 17.6083 59.1128 25.8083C61.4128 33.0083 71.8128 32.5083 75.1128 32.1083C75.6128 32.0083 75.7128 32.6083 75.1128 32.9083C72.2128 34.3083 56.7128 40.1083 50.3128 26.9083C50.3128 26.9083 41.9128 32.5083 37.5128 21.1083C37.3128 21.2083 25.7128 45.0083 0.212789 31.8083Z"
fill="currentColor"
/>
</svg>
</a>
</span>
</p>
</div>
</footer>

View file

@ -0,0 +1,101 @@
<script lang="ts">
import { navigating, page } from "$app/stores"
import Logomark from "$lib/components/Logomark.svelte"
import { Dialog, Label, useId } from "bits-ui"
import { Menu, X } from "lucide-svelte"
import { fade, fly } from "svelte/transition"
import SidebarItem from "./SidebarItem.svelte"
import TargetSelector from "./TargetSelector.svelte"
import VersionSelector from "./VersionSelector.svelte"
let dialogOpen = $state(false)
const [scope, name] = $page.data.pkg.name.split("/")
$effect(() => {
if ($navigating) {
dialogOpen = false
}
})
let versionOpen = $state(false)
let targetOpen = $state(false)
const versionId = useId()
const targetId = useId()
</script>
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Trigger>
<span class="sr-only">open menu</span>
<Menu aria-hidden="true" />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content forceMount>
{#snippet child({ props, open })}
{#if open}
<div {...props} class="fixed inset-0 top-0 z-50 flex flex-col">
<Dialog.Title class="sr-only">Menu</Dialog.Title>
<div transition:fade={{ duration: 200 }} class="bg-header">
<div
class="relative z-50 flex h-14 flex-shrink-0 items-center justify-between border-b px-4"
>
<a
class="flex items-center truncate"
href={`/packages/${scope}/${name}/${$page.params.version ?? "latest"}/${$page.params.target ?? "any"}`}
>
{#snippet separator()}
<span class="text-body/60 px-2 text-xl">/</span>
{/snippet}
<span class="text-primary mr-2">
<Logomark class="h-7" />
</span>
<span class="truncate">{scope}</span>
{@render separator()}
<span class="text-heading truncate font-medium">{name}</span>
</a>
<Dialog.Close>
<span class="sr-only">close menu</span>
<X aria-hidden="true" />
</Dialog.Close>
</div>
</div>
<div
class="bg-header flex flex-grow flex-col overflow-hidden"
transition:fade={{ duration: 200 }}
>
<nav
class="flex h-full flex-col overflow-y-auto p-4"
transition:fly={{ y: "-2%", duration: 200 }}
>
<div
class="mb-4 flex flex-col space-y-4 border-b pb-4 sm:flex-row sm:space-x-4 sm:space-y-0"
>
<div class="w-full">
<div class="text-heading mb-1 text-sm font-semibold">
<Label.Root for={versionId}>Version</Label.Root>
</div>
<VersionSelector id={versionId} bind:open={versionOpen} />
</div>
<div class="w-full">
<div class="text-heading mb-1 text-sm font-semibold">
<Label.Root for={targetId}>Target</Label.Root>
</div>
<TargetSelector id={targetId} bind:open={targetOpen} />
</div>
</div>
<ul>
{#each $page.data.sidebar ?? [] as item}
<li>
<SidebarItem {item} />
</li>
{/each}
</ul>
</nav>
</div>
</div>
{/if}
{/snippet}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>

View file

@ -0,0 +1,48 @@
<script lang="ts">
import type { TocItem } from "$lib/markdown"
import { Popover } from "bits-ui"
import { ChevronRight } from "lucide-svelte"
import { scale } from "svelte/transition"
import TocList, { tocScroll } from "./TocList.svelte"
import { activeHeaderId } from "./TocObserver.svelte"
type Props = {
toc: TocItem[]
}
const { toc }: Props = $props()
let activeHeaderTitle = $derived(toc.find((item) => item.id === activeHeaderId.value)?.title)
let popoverOpen = $state(false)
</script>
<nav class="bg-header/80 sticky top-14 border-b backdrop-blur-lg lg:ml-72 xl:hidden">
<div class="mx-auto flex h-12 max-w-screen-md items-center px-4 md:px-6">
<Popover.Root bind:open={popoverOpen}>
<Popover.Trigger
class="bg-background/80 flex h-8 flex-shrink-0 items-center rounded border px-4 text-sm font-semibold {popoverOpen
? 'border-primary'
: 'hover:border-body/50'}"
>
On this page
<ChevronRight class="ml-0.5 size-4" />
</Popover.Trigger>
<Popover.Content side="bottom" align="start" sideOffset={4} collisionPadding={8}>
{#snippet child({ props })}
<div
class="bg-card border-input-border max-h-[var(--bits-popover-content-available-height)] w-72 max-w-[var(--bits-popover-content-available-width)] origin-top-left overflow-y-auto rounded border p-4 pl-2 pr-6 shadow-lg"
use:tocScroll
transition:scale={{ start: 0.95, duration: 200 }}
{...props}
>
<TocList mobile {toc} />
</div>
{/snippet}
</Popover.Content>
</Popover.Root>
{#if activeHeaderTitle}
<span class="text-heading ml-2 truncate text-sm">{activeHeaderTitle}</span>
{/if}
</div>
</nav>

View file

@ -0,0 +1,36 @@
<script module lang="ts">
export type SidebarItem =
| {
label: string
href: string
}
| {
label: string
children: SidebarItem[]
collapsed: boolean
}
// this comment is here to fix the syntax highlighting
</script>
<script lang="ts">
import SidebarItemComponent from "./SidebarItem.svelte"
type Props = {
items: SidebarItem[]
}
const { items }: Props = $props()
</script>
<nav
class="sticky top-14 -mt-12 hidden h-[calc(100vh-theme(spacing.14))] w-72 flex-shrink-0 flex-col overflow-y-auto border-r p-4 lg:flex xl:mt-0"
>
<ul>
{#each items as item}
<li>
<SidebarItemComponent {item} />
</li>
{/each}
</ul>
</nav>

View file

@ -0,0 +1,45 @@
<script lang="ts">
import { page } from "$app/stores"
import { ChevronDownIcon } from "lucide-svelte"
import type { SidebarItem } from "./Sidebar.svelte"
import Self from "./SidebarItem.svelte"
type Props = {
item: SidebarItem
}
const { item }: Props = $props()
let open = $state("collapsed" in item ? !item.collapsed : true)
let active = $derived.by(() => {
if ("href" in item) {
const fullUrl = new URL(item.href, $page.url)
return fullUrl.pathname === $page.url.pathname
}
return false
})
</script>
{#if "href" in item}
<a
href={item.href}
class={`-mx-2 block rounded px-2 py-1.5 text-sm transition ${active ? "bg-primary-bg/20 text-primary font-semibold" : "hover:text-heading"}`}
>
{item.label}
</a>
{:else}
<details class="group flex flex-col py-0.5" bind:open>
<summary class="text-heading flex list-none items-center py-1 font-bold">
<span>{item.label} </span>
<ChevronDownIcon class="ml-auto size-5 transition group-[:not([open])]:-rotate-90" />
</summary>
<ul class="mb-1 border-l pl-4">
{#each item.children as child}
<li>
<Self item={child} />
</li>
{/each}
</ul>
</details>
{/if}

View file

@ -0,0 +1,26 @@
<script lang="ts">
import { page } from "$app/stores"
import type { Snippet } from "svelte"
type Props = {
tab: string
children: Snippet
active?: boolean
}
const { tab, children, active = false }: Props = $props()
const basePath = $derived.by(() => {
const { scope, name, version, target } = $page.params
return `/packages/${scope}/${name}/${version ?? "latest"}/${target ?? "any"}`
})
const href = $derived(`${basePath}/${tab}`)
</script>
<a
{href}
class={`-mb-px flex h-9 w-full items-center justify-center border-b-2 px-4 font-semibold transition sm:w-auto ${active ? "border-primary text-heading" : "hover:border-border hover:text-heading border-transparent"}`}
>
{@render children()}
</a>

View file

@ -0,0 +1,46 @@
<script lang="ts">
import { goto } from "$app/navigation"
import { page } from "$app/stores"
import Select from "$lib/components/Select.svelte"
import { TARGET_KIND_DISPLAY_NAMES, type TargetInfo } from "$lib/registry-api"
import type { Snippet } from "svelte"
let disabled = $state(false)
type Props = {
trigger?: Snippet<[Record<string, unknown>, string]>
sameWidth?: boolean
open?: boolean
id?: string
}
let { trigger, sameWidth = true, open = $bindable(false), id }: Props = $props()
const basePath = $derived.by(() => {
const { scope, name } = $page.params
return `/packages/${scope}/${name}`
})
</script>
<Select
items={$page.data.pkg.targets.map((target: TargetInfo) => ({
value: target.kind,
label: TARGET_KIND_DISPLAY_NAMES[target.kind],
}))}
value={$page.params.target ?? $page.data.pkg.targets[0].kind}
contentClass={sameWidth ? "" : "w-32"}
onValueChange={(target) => {
disabled = true
const path = $page.data.activeTab === "docs" ? "docs/intro" : "reference"
goto(`${basePath}/${$page.data.pkg.version}/${target}/${path}`).finally(() => {
disabled = false
})
}}
bind:open
{disabled}
{sameWidth}
{trigger}
{id}
/>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { TocItem } from "$lib/markdown"
import TocList, { tocScroll } from "./TocList.svelte"
type Props = {
toc: TocItem[]
}
const { toc }: Props = $props()
</script>
<aside
class="hide-scrollbar sticky top-14 hidden h-[calc(100vh-theme(spacing.14))] w-72 flex-shrink-0 overflow-y-auto py-16 text-lg xl:block"
use:tocScroll
>
<nav class="border-l pr-4 text-sm">
<h2 class="text-heading mb-1 pl-4 text-base font-semibold">On this page</h2>
<TocList {toc} />
</nav>
</aside>

View file

@ -0,0 +1,45 @@
<script module lang="ts">
import type { Action } from "svelte/action"
export const tocScroll: Action<HTMLElement> = (node) => {
$effect(() => {
const link = node.querySelector(`[data-item-id="${activeHeaderId.value}"]`)
if (link && link instanceof HTMLElement) {
setTimeout(() => {
node.scrollTo({
top: link.offsetTop + link.clientHeight - node.clientHeight / 2,
behavior: "smooth",
})
}, 20)
}
})
}
</script>
<script lang="ts">
import type { TocItem } from "$lib/markdown"
import { activeHeaderId } from "./TocObserver.svelte"
type Props = {
toc: TocItem[]
mobile?: boolean
}
const { toc, mobile = false }: Props = $props()
</script>
<ul>
{#each toc as item}
{@const active = item.id === activeHeaderId.value}
<li data-item-id={item.id}>
<a
href={`#${item.id}`}
class={`-ml-px block truncate pl-[calc(var(--level)*theme(spacing.4))] transition ${active ? "text-primary" : "hover:text-heading"} ${mobile ? "py-2" : "py-1"}`}
style:--level={item.level - 1}
>
{item.title}
</a>
</li>
{/each}
</ul>

View file

@ -0,0 +1,80 @@
<script module lang="ts">
let _activeHeaderId = $state("")
export const activeHeaderId = {
get value() {
return _activeHeaderId
},
set value(value) {
_activeHeaderId = value
},
}
</script>
<script lang="ts">
import type { TocItem } from "$lib/markdown"
type Props = {
toc: TocItem[]
}
const { toc }: Props = $props()
$effect(() => {
toc
const isHeading = (el: Element): boolean => {
if (el.id === "_top") return true
if (el instanceof HTMLHeadingElement) {
const level = el.tagName[1]
if (level) {
const int = parseInt(level, 10)
if (int >= 2 && int <= 3) return true
}
}
return false
}
const getNearestHeading = (el: Element | null): Element | null => {
if (!el) return null
while (el) {
if (isHeading(el)) return el
el = el.previousElementSibling
}
return null
}
const callback: IntersectionObserverCallback = (entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
const heading = getNearestHeading(entry.target)
if (!heading) continue
const item = toc.find((item) => {
if (item.id === "_top" && heading.id === "_top") return true
return `user-content-${item.id}` === heading.id
})
if (item) {
activeHeaderId.value = item.id
}
}
}
const observer = new IntersectionObserver(callback, {
rootMargin: "-56px 0px -80% 0px",
})
const targets = document.querySelectorAll("main > *")
for (const target of targets) {
observer.observe(target)
}
return () => {
observer.disconnect()
}
})
</script>

View file

@ -0,0 +1,48 @@
<script lang="ts">
import { goto } from "$app/navigation"
import { page } from "$app/stores"
import Select from "$lib/components/Select.svelte"
import { fetchRegistryJson, type PackageVersionResponse } from "$lib/registry-api"
import type { Snippet } from "svelte"
let disabled = $state(false)
type Props = {
trigger?: Snippet<[Record<string, unknown>, string]>
sameWidth?: boolean
open?: boolean
id?: string
}
let { trigger, sameWidth = true, open = $bindable(false), id }: Props = $props()
const basePath = $derived.by(() => {
const { scope, name } = $page.params
return `/packages/${scope}/${name}`
})
</script>
<Select
items={$page.data.versions.map((v: string) => ({ value: v, label: v }))}
value={$page.data.pkg.version}
contentClass={sameWidth ? "" : "w-32"}
onValueChange={(version) => {
disabled = true
const path = $page.data.activeTab === "docs" ? "docs/intro" : "reference"
fetchRegistryJson<PackageVersionResponse>(
`packages/${encodeURIComponent($page.data.pkg.name)}/${version}/any`,
fetch,
)
.then((pkg) => goto(`${basePath}/${version}/${pkg.targets[0].kind}/${path}`))
.finally(() => {
disabled = false
})
}}
bind:open
{disabled}
{sameWidth}
{trigger}
{id}
/>

View file

@ -0,0 +1,8 @@
<script>
import { page } from "$app/stores"
</script>
<div class="mx-auto max-w-screen-xl px-4 py-32 text-center">
<h1 class="text-heading mb-1 text-4xl font-bold">{$page.status}</h1>
<p class="text-lg">{$page.error?.message}</p>
</div>

View file

@ -0,0 +1,33 @@
import type { DocEntry } from "$lib/registry-api"
import type { SidebarItem } from "../Sidebar.svelte"
import type { LayoutLoad } from "./$types"
export const load: LayoutLoad = async ({ params, parent }) => {
const parentData = await parent()
const { scope, name, version, target } = params
const basePath = `/packages/${scope}/${name}/${version ?? "latest"}/${target ?? "any"}`
function docEntryToSidebarItem(entry: DocEntry): SidebarItem {
if ("name" in entry) {
return {
label: entry.label,
href: `${basePath}/docs/${entry.name}`,
}
}
return {
label: entry.label,
children: entry.items?.map(docEntryToSidebarItem) ?? [],
collapsed: entry.collapsed ?? false,
}
}
const sidebar = parentData.pkg.docs?.map(docEntryToSidebarItem) ?? []
return {
activeTab: "docs",
doc: params.doc,
sidebar,
}
}

View file

@ -0,0 +1,6 @@
import { redirect } from "@sveltejs/kit"
import type { PageLoad } from "./$types"
export const load: PageLoad = async () => {
redirect(301, "docs/intro")
}

View file

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

View file

@ -0,0 +1,62 @@
import { docsMarkdown, type TocItem } from "$lib/markdown"
import { fetchRegistry, RegistryHttpError, type DocEntry } from "$lib/registry-api"
import { error } from "@sveltejs/kit"
import { VFile } from "vfile"
import type { PageLoad } from "./$types"
const findDocTitle = (docs: DocEntry[], name: string): string | undefined => {
for (const doc of docs) {
if ("name" in doc && doc.name === name) return doc.label
if ("items" in doc && doc.items) {
const title = findDocTitle(doc.items, name)
if (title) return title
}
}
return undefined
}
export const load: PageLoad = async ({ params, parent, fetch }) => {
try {
const page = await fetchRegistry(
`packages/${encodeURIComponent(`${params.scope}/${params.name}`)}/${params.version ?? "latest"}/${params.target ?? "any"}?doc=${encodeURIComponent(params.doc)}`,
fetch,
).then((r) => r.text())
const inputFile = new VFile({
path: `/docs/${params.doc}`,
value: page,
data: {
basePath: `/packages/${params.scope}/${params.name}/${params.version ?? "latest"}/${params.target ?? "any"}`,
},
})
const file = await (await docsMarkdown).process(inputFile)
const html = file.value
const parentData = await parent()
const docTitle = findDocTitle(parentData.pkg.docs ?? [], params.doc) ?? params.doc
return {
html,
toc: [
{
id: "_top",
title: "Overview",
level: 2,
},
...(file.data.toc as TocItem[]),
],
meta: {
siteName: `${parentData.pkg.name} - pesde`,
title: docTitle,
description: (file.data.meta as { description: string }).description,
},
}
} catch (e) {
if (e instanceof RegistryHttpError && e.response.status === 404) {
error(404, "Page not found")
}
throw e
}
}

View file

@ -0,0 +1,29 @@
import type { LayoutLoad } from "./$types"
export const load: LayoutLoad = async ({ params, parent }) => {
const { scope, name, version, target } = params
const basePath = `/packages/${scope}/${name}/${version ?? "latest"}/${target ?? "any"}`
const parentData = await parent()
return {
activeTab: "reference",
sidebar: [
{
label: "Reference",
href: `${basePath}/reference`,
},
],
toc: [
{
id: "_top",
title: "Overview",
level: 2,
},
],
meta: {
siteName: `${parentData.pkg.name} - pesde`,
title: "Reference",
description: `API reference for ${parentData.pkg.name}`,
},
}
}

View file

@ -0,0 +1,10 @@
<script>
import { Boxes } from "lucide-svelte"
</script>
<main class="mx-auto max-w-screen-2xl px-4 py-48 text-center">
<Boxes aria-hidden="true" class="mx-auto mb-8 size-16" />
<h1 class="text-heading mb-1 text-xl font-bold">Reference coming soon!</h1>
<p>The reference will contain API documentation for packages.</p>
</main>

View file

@ -0,0 +1,136 @@
<script lang="ts">
import { page } from "$app/stores"
import GitHub from "$lib/components/GitHub.svelte"
import type { TargetInfo } from "$lib/registry-api"
import { BinaryIcon, Globe, Icon, LibraryIcon, Mail } from "lucide-svelte"
import type { ComponentType } from "svelte"
import TargetSelector from "../../TargetSelector.svelte"
import Command from "./Command.svelte"
let { children, data } = $props()
const installCommand = $derived(`pesde add ${data.pkg.name}`)
const xCommand = $derived(`pesde x ${data.pkg.name}`)
const defaultTarget = $derived(
"target" in $page.params && $page.params.target !== "any"
? $page.params.target
: data.pkg.targets[0].kind,
)
const currentTarget = $derived(
data.pkg.targets.find((target: TargetInfo) => target.kind === defaultTarget),
)
const repositoryUrl = $derived(
data.pkg.repository !== undefined ? new URL(data.pkg.repository) : undefined,
)
const isGitHub = $derived(repositoryUrl?.hostname === "github.com")
const githubRepo = $derived(
repositoryUrl?.pathname
.split("/")
.slice(1, 3)
.join("/")
.replace(/\.git$/, ""),
)
const exportNames: Partial<Record<keyof TargetInfo, string>> = {
lib: "Library",
bin: "Binary",
}
const exportIcons: Partial<Record<keyof TargetInfo, ComponentType<Icon>>> = {
lib: LibraryIcon,
bin: BinaryIcon,
}
const exportEntries = $derived(
Object.entries(exportNames).filter(([key]) => !!currentTarget?.[key as keyof TargetInfo]),
)
</script>
<div class="flex flex-col lg:flex-row">
<div class="flex-grow lg:pr-4">
{@render children()}
</div>
<aside
class="w-full flex-shrink-0 border-t pt-16 lg:ml-auto lg:max-w-[22rem] lg:border-l lg:border-t-0 lg:pl-4 lg:pt-6"
>
<h2 class="text-heading mb-1 text-lg font-semibold">Install</h2>
<Command command={installCommand} class="mb-4" />
<div class="hidden lg:block">
<TargetSelector />
</div>
{#if data.pkg.license !== undefined}
<h2 class="text-heading mb-1 text-lg font-semibold">License</h2>
<div class="mb-6">{data.pkg.license}</div>
{/if}
{#if data.pkg.repository !== undefined}
<h2 class="text-heading mb-1 text-lg font-semibold">Repository</h2>
<div class="mb-6">
<a
href={data.pkg.repository}
class="inline-flex items-center space-x-2 underline"
target="_blank"
rel="noreferrer noopener"
>
{#if isGitHub}
<GitHub class="text-primary size-5" />
<span>
{githubRepo}
</span>
{:else}
{data.pkg.repository}
{/if}
</a>
</div>
{/if}
<h2 class="text-heading mb-1 text-lg font-semibold">Exports</h2>
<ul class="mb-6 space-y-0.5">
{#each exportEntries as [exportKey, exportName]}
{@const Icon = exportIcons[exportKey as keyof TargetInfo]}
<li class="flex items-center">
<Icon aria-hidden="true" class="text-primary mr-2 size-5" />
{exportName}
</li>
{/each}
</ul>
{#if currentTarget?.bin}
<p class="text-body/80 -mt-3 mb-4 text-sm">
This package provides a binary that can be executed after installation, or globally via:
</p>
<Command command={xCommand} class="mb-6" />
{/if}
{#if data.pkg.authors && data.pkg.authors.length > 0}
<h2 class="text-heading mb-2 text-lg font-semibold">Authors</h2>
<ul>
{#each data.pkg.authors as author}
{@const [, name] = author.match(/^(.*?)\s*(<|\(|$)/) ?? []}
{@const [, email] = author.match(/<(.*)>/) ?? []}
{@const [, website] = author.match(/\((.*)\)/) ?? []}
<li class="mb-2 flex items-center">
{name}
<div class="ml-auto flex items-center space-x-2">
{#if email}
<a href={`mailto:${email}`} class="text-primary ml-1" title={`Email: ${email}`}>
<Mail class="text-primary size-5" aria-hidden="true" />
</a>
{/if}
{#if website}
<a href={website} class="text-primary ml-1" title={`Website: ${website}`}>
<Globe class="text-primary size-5" aria-hidden="true" />
</a>
{/if}
</div>
</li>
{/each}
</ul>
{/if}
</aside>
</div>

View file

@ -0,0 +1,8 @@
<script>
const { data } = $props()
</script>
<div class="prose min-w-0 py-8 prose-pre:w-full prose-pre:overflow-auto">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html data.readmeHtml}
</div>

View file

@ -0,0 +1,51 @@
import { markdown } from "$lib/markdown"
import { fetchRegistry, RegistryHttpError } from "$lib/registry-api"
import type { PageLoad } from "./$types"
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: PageLoad = async ({ parent, fetch }) => {
const { pkg } = await parent()
const { name, version, targets } = pkg
const readmeText = await fetchReadme(fetch, name, version, targets[0].kind)
const file = await (await markdown)
.process(readmeText)
const readmeHtml = file.value
return {
readmeHtml,
pkg,
meta: {
title: `${name} - ${version}`,
description: pkg.description,
},
}
}

View file

@ -0,0 +1,36 @@
<script lang="ts">
import { Check, Clipboard } from "lucide-svelte"
type Props = {
command: string
class?: string
}
const { command, class: classname = "" }: Props = $props()
let didCopy = $state(false)
</script>
<div class={`flex h-11 items-center overflow-hidden rounded border text-sm ${classname}`}>
<code class="truncate px-4">{command}</code>
<button
class="bg-card/40 hover:bg-card/60 ml-auto flex size-11 items-center justify-center border-l"
onclick={() => {
navigator.clipboard.writeText(command)
if (didCopy) return
didCopy = true
setTimeout(() => {
didCopy = false
}, 1000)
}}
>
<span class="sr-only">Copy</span>
{#if didCopy}
<Check class="size-5" aria-hidden="true" />
{:else}
<Clipboard class="size-5" aria-hidden="true" />
{/if}
</button>
</div>

View file

@ -0,0 +1,93 @@
<script lang="ts">
import { page } from "$app/stores"
import { DEPENDENCY_KIND_DISPLAY_NAMES, type DependencyKind } from "$lib/registry-api.js"
const { data } = $props()
// Vercel only supports up to Node 20.x, which doesn't support Object.groupBy
function groupBy<T, K extends PropertyKey>(
arr: T[],
predicate: (value: T) => K,
): Partial<Record<K, T[]>> {
const groups: Partial<Record<K, T[]>> = {}
for (const item of arr) {
const key = predicate(item)
if (key in groups) {
groups[key]!.push(item)
} else {
groups[key] = [item]
}
}
return groups
}
let groupedDeps = $derived(
groupBy(
Object.entries(data.pkg.dependencies).map(([alias, dependency]) => ({ alias, dependency })),
(entry) => entry.dependency[1],
),
)
const stripWally = (s: string) => s.replace(/^wally#/, "")
</script>
{#if Object.keys(groupedDeps).length === 0}
<p class="py-24 text-center">This package doesn't have any dependencies.</p>
{:else}
<div class="space-y-8 py-8">
{#each Object.entries(groupedDeps).sort( (a, b) => b[0].localeCompare(a[0]), ) as [dependencyKind, group]}
<section>
<h2 class="text-heading mb-4 text-xl font-medium">
{DEPENDENCY_KIND_DISPLAY_NAMES[dependencyKind as DependencyKind]}
</h2>
<div class="space-y-4">
{#each group as { dependency: [dependencyInfo] }}
{@const isWally = "wally" in dependencyInfo}
{@const [scope, name] = (
isWally ? stripWally(dependencyInfo.wally) : dependencyInfo.name
).split("/")}
{@const target = isWally
? undefined
: (dependencyInfo.target ?? $page.params.target ?? data.pkg.targets[0].kind)}
{@const isOfficialRegistry = isWally
? dependencyInfo.index.toLowerCase() === "https://github.com/upliftgames/wally-index"
: dependencyInfo.index.toLowerCase() === "https://github.com/daimond113/pesde-index"}
<article
class={`bg-card relative overflow-hidden rounded px-5 py-4 transition ${
isOfficialRegistry ? "hover:bg-card-hover" : ""
}`}
>
<h3 class="font-semibold">
<svelte:element
this={isOfficialRegistry ? "a" : "svelte:fragment"}
{...isOfficialRegistry
? {
href: isWally
? `https://wally.run/package/${stripWally(dependencyInfo.wally)}`
: `/packages/${dependencyInfo.name}/latest/${target}`,
}
: {}}
class="after:absolute after:inset-0 after:content-['']"
>
<span class="text-heading">{scope}/</span><span class="text-light">{name}</span>
{#if isWally}
<span class="text-red-400">(wally)</span>
{/if}
</svelte:element>
</h3>
<div class="text-primary text-sm font-semibold">
{dependencyInfo.version}
{#if !isWally}
·
{target}
{/if}
</div>
</article>
{/each}
</div>
</section>
{/each}
</div>
{/if}

View file

@ -0,0 +1,12 @@
import type { PageLoad } from "./$types"
export const load: PageLoad = async ({ parent }) => {
const data = await parent()
return {
meta: {
title: `${data.pkg.name} - ${data.pkg.version} - dependencies`,
description: data.pkg.description,
},
}
}

View file

@ -0,0 +1,46 @@
<script lang="ts">
import { TARGET_KIND_DISPLAY_NAMES, type TargetInfo } from "$lib/registry-api.js"
import { formatDistanceToNow } from "date-fns"
const { data } = $props()
let displayDates = $state(false)
$effect(() => {
displayDates = true
})
</script>
<div class="space-y-4 py-4">
{#each data.versions as pkg, index}
{@const isLatest = index === 0}
<article
class={`bg-card hover:bg-card-hover relative overflow-hidden rounded px-5 py-4 transition ${
isLatest ? "ring-primary ring-2 ring-inset" : ""
}`}
>
<h2 class="text-heading font-semibold">
<a
href={`/packages/${pkg.name}/${pkg.version}/any`}
class="after:absolute after:inset-0 after:content-['']"
>
{pkg.version}
{#if isLatest}
<span class="text-primary">(latest)</span>
{/if}
</a>
</h2>
<div class="text-sm font-semibold" class:invisible={!displayDates}>
<time datetime={pkg.published_at}>
{#if displayDates}
{formatDistanceToNow(new Date(pkg.published_at), { addSuffix: true })}
{:else}
...
{/if}
</time>
·
{pkg.targets.map((target: TargetInfo) => TARGET_KIND_DISPLAY_NAMES[target.kind]).join(", ")}
</div>
</article>
{/each}
</div>

View file

@ -0,0 +1,34 @@
import {
fetchRegistryJson,
RegistryHttpError,
type PackageVersionsResponse,
} from "$lib/registry-api"
import { error } from "@sveltejs/kit"
import type { PageLoad } from "./$types"
export const load: PageLoad = async ({ params, fetch }) => {
const { scope, name } = params
try {
const versions = await fetchRegistryJson<PackageVersionsResponse>(
`packages/${encodeURIComponent(`${scope}/${name}`)}`,
fetch,
)
versions.reverse()
return {
versions,
meta: {
title: `${versions[0].name} - versions`,
description: versions[0].description,
},
}
} catch (e) {
if (e instanceof RegistryHttpError && e.response.status === 404) {
error(404, "Package not found")
}
throw e
}
}

View file

@ -0,0 +1,10 @@
let _searchQuery = $state("")
export const searchQuery = {
get value() {
return _searchQuery
},
set value(value: string) {
_searchQuery = value
},
}

View file

@ -0,0 +1,169 @@
<script lang="ts">
import { goto } from "$app/navigation"
import { TARGET_KIND_DISPLAY_NAMES } from "$lib/registry-api.js"
import { Pagination } from "bits-ui"
import { formatDistanceToNow } from "date-fns"
import { ChevronLeft, ChevronRight, SearchX } from "lucide-svelte"
const { data } = $props()
let displayDates = $state(false)
$effect(() => {
displayDates = true
})
</script>
<div class="mx-auto max-w-screen-lg px-4">
{#await data.result}
<div class="sr-only">Loading...</div>
<div
class="my-8 flex flex-col sm:flex-row sm:items-center sm:justify-between"
aria-hidden="true"
>
<div class="bg-card-hover w-20 animate-pulse rounded text-transparent">...</div>
<div class="bg-card-hover mx-auto mt-8 h-8 w-48 animate-pulse rounded sm:m-0"></div>
</div>
<div class="mb-8 space-y-4">
{#each Array.from({ length: 10 }).map((_, i) => i) as i}
<div class="bg-card/50 overflow-hidden rounded px-5 py-4">
<div class="mb-1 flex items-center justify-between">
<div class="bg-card-hover w-52 animate-pulse rounded text-xl text-transparent">...</div>
<div
class="bg-card-hover hidden w-32 animate-pulse rounded text-sm text-transparent sm:block"
>
...
</div>
</div>
<div
class="bg-card-hover mb-3 w-96 max-w-full animate-pulse rounded text-sm text-transparent"
>
...
</div>
<div class="bg-card-hover w-16 max-w-full animate-pulse rounded text-sm text-transparent">
...
</div>
</div>
{/each}
</div>
<div class="bg-card-hover mx-auto my-8 h-8 w-48 animate-pulse rounded"></div>
{:then result}
{#if result.data.length === 0}
<div class="flex flex-col items-center py-32 text-center">
<SearchX class="mb-6 size-16" />
<h1 class="text-heading font-bold">No results</h1>
<p class="text-balance">We didn't find any packages matching your search query.</p>
</div>
{:else}
{#snippet pagination()}
<Pagination.Root
count={result.count}
page={data.page}
perPage={data.pageSize}
onPageChange={(page) => {
const params = new URLSearchParams()
params.set("q", data.query)
params.set("page", page.toString())
goto(`/search?${params}`)
}}
let:pages
>
<div class="flex items-center space-x-1">
<Pagination.PrevButton
class="hover:enabled:bg-card-hover inline-flex size-8 items-center justify-center rounded transition disabled:opacity-50"
>
<ChevronLeft />
</Pagination.PrevButton>
{#each pages as page (page.key)}
{#if page.type === "ellipsis"}
<div class="px-2">...</div>
{:else}
<Pagination.Page
{page}
class="hover:bg-card-hover hover:text-heading data-[selected]:bg-primary-bg data-[selected]:text-primary-fg inline-flex size-8 items-center justify-center rounded font-bold transition"
>
{page.value}
</Pagination.Page>
{/if}
{/each}
<Pagination.NextButton
class="hover:enabled:bg-card-hover inline-flex size-8 items-center justify-center rounded transition disabled:opacity-50"
>
<ChevronRight />
</Pagination.NextButton>
</div>
</Pagination.Root>
{/snippet}
<div class="my-8 flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h1 class="font-bold">{result.count} {result.count > 1 ? "results" : "result"}</h1>
<div class="mx-auto mt-8 sm:m-0">
{@render pagination()}
</div>
</div>
<div class="mb-8 space-y-4">
{#each result.data as pkg}
{@const [scope, name] = pkg.name.split("/")}
{#snippet timeAndVersion()}
<time datetime={pkg.published_at}>
{#if displayDates}
{formatDistanceToNow(new Date(pkg.published_at), { addSuffix: true })}
{:else}
...
{/if}
</time>
<span>{" · "}</span>
<span class="truncate">v{pkg.version}</span>
{/snippet}
<article
class="bg-card hover:bg-card-hover relative overflow-hidden rounded px-5 py-4 transition"
>
<div class="mb-1 flex items-center justify-between">
<h3 class="truncate text-xl font-semibold">
<a
href={`/packages/${pkg.name}`}
class="after:absolute after:inset-0 after:content-['']"
>
<span class="text-heading">{scope}/</span><span class="text-light">{name}</span>
</a>
</h3>
<div
class="text-heading hidden text-sm font-semibold sm:block"
class:invisible={!displayDates}
>
{@render timeAndVersion()}
</div>
</div>
<p class="mb-3 h-[1lh] overflow-hidden truncate text-sm">{pkg.description}</p>
<div
class={`text-primary text-sm font-bold ${displayDates ? "" : "invisible sm:visible"}`}
>
{pkg.targets.map((target) => TARGET_KIND_DISPLAY_NAMES[target.kind]).join(", ")}
<span class="sm:hidden">
<span>{" · "}</span>
{@render timeAndVersion()}
</span>
</div>
</article>
{/each}
</div>
<div class="mx-auto my-8 max-w-min">
{@render pagination()}
</div>
{/if}
{:catch error}
<div class="mx-auto max-w-screen-xl px-4 py-32 text-center">
<h1 class="text-heading mb-1 text-4xl font-bold">Error</h1>
<p class="text-lg">{error.message}</p>
</div>
{/await}
</div>

View file

@ -0,0 +1,31 @@
import { fetchRegistryJson, type SearchResponse } from "$lib/registry-api"
import type { PageLoad } from "./$types"
const PAGE_SIZE = 50
export const load: PageLoad = async ({ fetch, url }) => {
const query = url.searchParams.get("q") ?? ""
let page = parseInt(url.searchParams.get("page") ?? "1")
if (isNaN(page) || page < 1) {
page = 1
}
const params = new URLSearchParams()
params.set("query", query)
params.set("offset", String((page - 1) * PAGE_SIZE))
const result = fetchRegistryJson<SearchResponse>(`search?${params}`, fetch)
return {
query,
page,
pageSize: PAGE_SIZE,
result,
meta: {
title: query === "" ? "search" : `results for "${query}"`,
description: "Search for packages in the pesde registry.",
},
}
}

View file

@ -0,0 +1,8 @@
<script>
import { page } from "$app/stores"
</script>
<div class="mx-auto max-w-screen-xl px-4 py-32 text-center">
<h1 class="text-heading mb-1 text-4xl font-bold">{$page.status}</h1>
<p class="text-lg">{$page.error?.message}</p>
</div>

View file

@ -0,0 +1,78 @@
<script lang="ts">
import { page } from "$app/stores"
import "@fontsource-variable/nunito-sans"
import "../app.css"
const { children } = $props()
const siteName = $derived($page.data.meta?.siteName ?? "pesde")
const title = $derived($page.data.meta?.title)
const description = $derived(
$page.data.meta?.description ??
"A package manager for the Luau programming language, supporting multiple runtimes including Roblox and Lune.",
)
let themeColor = $state("#F19D1E")
$effect(() => {
const query = window.matchMedia("(prefers-color-scheme: dark)")
const updateColor = (dark: boolean) => {
themeColor = dark ? "#14100C" : "#FAEAD7"
}
const listener = (e: MediaQueryListEvent) => {
updateColor(e.matches)
}
query.addEventListener("change", listener)
updateColor(query.matches)
return () => query.removeEventListener("change", listener)
})
function hashChange() {
let hash
try {
hash = decodeURIComponent(location.hash.slice(1)).toLowerCase()
} catch {
return
}
const id = "user-content-" + hash
const target = document.getElementById(id)
if (target) {
target.scrollIntoView()
}
}
$effect(() => {
hashChange()
})
</script>
<svelte:head>
<title>{title ? `${title} - ${siteName}` : siteName}</title>
<meta name="description" content={description} />
<meta name="theme-color" content={themeColor} />
<meta property="og:site_name" content={siteName} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title ?? "Manage your packages for Luau"} />
<meta property="og:description" content={description} />
<meta property="og:image" content="/favicon-48x48.png" />
<meta name="twitter:card" content="summary" />
<link rel="icon" type="image/png" href="/favicon-48x48.png" sizes="48x48" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="pesde" />
<link rel="manifest" href="/site.webmanifest" />
</svelte:head>
<svelte:window onhashchange={hashChange} />
{@render children()}

Some files were not shown because too many files have changed in this diff Show more