WIP: feat(webserver): Vendor and reimplement in leptos

This commit is contained in:
Tristan Daniël Maat 2025-11-24 03:29:18 +08:00
parent aeba7301b0
commit 948f2e6a6a
Signed by: tlater
GPG key ID: 02E935006CF2E8E7
19 changed files with 3767 additions and 166 deletions

View file

@ -1,5 +1,8 @@
{ pkgs }:
{ pkgs, flake-inputs }:
let
inherit (flake-inputs.nixpkgs-unstable.legacyPackages.${pkgs.system}) ast-grep;
in
pkgs.lib.packagesFromDirectoryRecursive {
inherit (pkgs) callPackage;
callPackage = pkgs.lib.callPackageWith (pkgs // { inherit ast-grep; });
directory = ./packages;
}

1
pkgs/packages/webserver/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

3030
pkgs/packages/webserver/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,61 @@
[package]
name = "tlaternet-webserver"
version = "0.2.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.8.7", optional = true }
console_error_panic_hook = { version = "0.1.7", optional = true }
leptos = "0.8.3"
leptos_axum = { version = "0.8.3", optional = true }
leptos_meta = "0.8.3"
leptos_router = "0.8.3"
markdown_view_leptos = "0.1.3"
tokio = { version = "1.48.0", features = ["rt-multi-thread"], optional = true }
wasm-bindgen = { version = "=0.2.100", optional = true }
[features]
hydrate = [
"leptos/hydrate",
"dep:console_error_panic_hook",
"dep:wasm-bindgen",
]
ssr = [
"dep:axum",
"dep:tokio",
"dep:leptos_axum",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.leptos]
output-name = "tlaternet-webserver"
site-root = "target/site"
site-pkg-dir = "pkg"
style-file = "style/main.scss"
site-addr = "127.0.0.1:3000"
reload-port = 3001
browserquery = "defaults"
env = "DEV"
bin-features = ["ssr"]
bin-default-features = false
lib-features = ["hydrate"]
lib-default-features = false
lib-profile-release = "wasm-release"
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"

View file

@ -0,0 +1,5 @@
max_width = 100
tab_spaces = 2
attr_value_brace_style = "WhenRequired"
macro_names = [ "leptos::view", "view" ]
closing_tag_style = "SelfClosing"

View file

@ -0,0 +1,227 @@
{
lib,
stdenvNoCC,
fetchFromGitHub,
fetchurl,
symlinkJoin,
makeBinaryWrapper,
cargo-leptos,
dart-sass,
llvmPackages,
mkShell,
clangStdenv,
rust-analyzer,
rustc,
rustfmt,
leptosfmt,
cargo,
clippy,
writers,
ast-grep,
nix-prefetch-github,
}:
let
cargoMetadata = lib.pipe ./Cargo.toml [
builtins.readFile
builtins.fromTOML
];
sass-dependencies = {
bulma = stdenvNoCC.mkDerivation (drv: {
pname = "bulma";
version = "1.0.4";
src = fetchFromGitHub {
owner = "jgthms";
repo = "bulma";
rev = drv.version;
hash = "sha256-hlejqBI6ayzhm15IymrzhTevkl3xffMfdTasZ2CmAas=";
};
installPhase = ''
mkdir -p $out/node_modules/bulma/
cp -r ./sass $out/node_modules/bulma/
'';
});
fontsource-scss = stdenvNoCC.mkDerivation {
pname = "fontsource-scss";
version = "0.2.2";
src = fetchurl {
url = "https://registry.npmjs.org/@fontsource-utils/scss/-/scss-0.2.2.tgz";
hash = "sha256-2BkCBhh01kZfMHhjHMMLDtUeesi7Uy7eMoeM1BAqX38=";
};
installPhase = ''
mkdir -p $out/node_modules/@fontsource-utils/scss/
cp -r . $out/node_modules/@fontsource-utils/scss/
'';
};
fontsource-nunito = stdenvNoCC.mkDerivation {
pname = "fontsource-nunito";
version = "5.2.7";
src = fetchurl {
url = "https://registry.npmjs.org/@fontsource-variable/nunito/-/nunito-5.2.7.tgz";
hash = "sha256-xSt1sDpVL/hVYzffKTgN/t7uLI3JadDWtTfWow2jiPM=";
};
outputs = [
"out"
"assets"
];
installPhase = ''
mkdir -p $out/node_modules/@fontsource-variable/nunito/
cp -r . $out/node_modules/@fontsource-variable/nunito/
mkdir -p $assets/@fontsource-variable/
cp -r files/ $assets/@fontsource-variable/nunito
'';
};
fontsource-arimo = stdenvNoCC.mkDerivation {
pname = "fontsource-nunito";
version = "5.2.8";
src = fetchurl {
url = "https://registry.npmjs.org/@fontsource-variable/arimo/-/arimo-5.2.8.tgz";
hash = "sha256-jD1IGqy02j4bqMRAwbCgiIz/h97WPrTSd3eZ09nptHA=";
};
outputs = [
"out"
"assets"
];
installPhase = ''
mkdir -p $out/node_modules/@fontsource-variable/arimo/
cp -r . $out/node_modules/@fontsource-variable/arimo/
mkdir -p $assets/@fontsource-variable/
cp -r files/ $assets/@fontsource-variable/arimo
'';
};
};
other-dependencies = {
hack-font = stdenvNoCC.mkDerivation (drv: {
pname = "hack-font";
version = "3.003";
src = fetchFromGitHub {
owner = "source-foundry";
repo = "Hack";
rev = "v${drv.version}";
hash = "sha256-qGDtBvKecdfsleUBfXFezllz9Op679a030Qcj/oBs1o=";
};
dontBuild = true;
installPhase = ''
mkdir -p $out
cp -r build/web/fonts $out/hack-font/
'';
});
};
assets = symlinkJoin {
name = "assets-${cargoMetadata.package.name}";
paths = [
sass-dependencies.fontsource-arimo.assets
sass-dependencies.fontsource-nunito.assets
other-dependencies.hack-font
];
};
# Hack to allow importing sass *without* resorting to using `npm`
# and dealing with the insanity that is `package.json` files.
#
# dart-sass *in theory* supports completely arbitrary logic for
# package importing via the `pkg:` url prefix, but unfortunately
# currently the only implementation of it that is available in the
# upstream binary *requires* using a `node_modules` file in the
# repository root. See here:
# https://sass-lang.com/documentation/at-rules/use/#node-js-package-importer
#
# This wouldn't be so bad if it supported an environment variable or
# command line arg to specify what the repo root should be, but it
# doesn't; so instead we use the load-path to specify a package
# import root.
#
# As a consequence, we cannot use the `pkg:` prefix, so package
# imports are indistinguishable from relative path imports. This
# isn't the end of the world, but:
#
# TODO(tlater): See if we can talk to upstream about an
# implementation better suited for use with nix, perhaps in
# dart-sass (add an env variable?) or in cargo-leptos (add a config
# option that can also be set with an env variable, and use the sass
# protocol instead of the raw exe?).
dart-sass-with-packages =
let
packages = symlinkJoin {
name = "sass-packages";
paths = lib.attrValues sass-dependencies;
stripPrefix = "/node_modules";
};
in
symlinkJoin {
inherit (dart-sass) version;
pname = "dart-sass-with-packages";
paths = [ dart-sass ];
nativeBuildInputs = [ makeBinaryWrapper ];
postBuild = ''
wrapProgram $out/bin/sass \
--add-flag --load-path=${packages}
'';
};
in
symlinkJoin {
inherit (cargoMetadata.package) version;
pname = cargoMetadata.package.name;
paths = [ ];
passthru = {
dependencies = sass-dependencies;
devShell = mkShell.override { stdenv = clangStdenv; } {
packages = [
cargo-leptos
dart-sass-with-packages
# lld is exposed as ld by the clangStdenv, adding it
# explicitly with bintools makes it work
llvmPackages.bintools
rust-analyzer
rustc
rustfmt
leptosfmt
cargo
clippy
];
LEPTOS_ASSETS_DIR = assets.outPath;
};
updateScript = writers.writeNuBin "update-${cargoMetadata.package.name}" {
makeWrapperArgs = [
"--prefix"
"PATH"
":"
(lib.makeBinPath [
ast-grep
nix-prefetch-github
])
];
} ./update.nu;
};
}

View file

@ -0,0 +1,2 @@
[rustfmt]
overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"]

View file

@ -0,0 +1,94 @@
use leptos::prelude::*;
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
use leptos_router::{
StaticSegment,
components::{Route, Router, Routes},
};
mod homepage;
use homepage::HomePage;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="description" content="tlater.net homepage" />
<meta name="author" content="Tristan Daniël Maat" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<AutoReload options=options.clone() />
<HydrationScripts options />
<MetaTags />
</head>
<body>
<App />
</body>
</html>
}
}
#[component]
pub fn Navbar() -> impl IntoView {
view! {
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item has-text-primary is-uppercase" href="/">
tlater
</a>
<a
class="navbar-burger"
role="button"
aria-label="menu"
aria-expanded="false"
data-target="main-navigation"
>
<span aria-hidden="true" />
<span aria-hidden="true" />
<span aria-hidden="true" />
</a>
</div>
<div id="main-navigation" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="~/src/mail.html">
"E-Mail"
</a>
<a class="navbar-item" href="https://www.gitlab.com/tlater">
GitLab
</a>
<a class="navbar-item" href="https://www.github.com/TLATER">
GitHub
</a>
</div>
</div>
</nav>
}
}
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/tlaternet-webserver.css" />
// sets the document title
<Title text="Welcome to Leptos" />
<Navbar />
// content for this welcome page
<Router>
<main>
<Routes fallback=|| "Page not found.".into_view()>
<Route path=StaticSegment("") view=HomePage />
</Routes>
</main>
</Router>
}
}

View file

@ -0,0 +1,80 @@
use leptos::prelude::*;
use markdown_view_leptos::markdown_view;
#[component]
pub fn HomePage() -> impl IntoView {
view! {
<section class="section">
<div class="container">
<h1 class="title has-text-weight-normal is-family-monospace">
<span id="typed-welcome">"$ Welcome to tlater.net!"</span>
</h1>
<hr />
<div class="columns">
<div class="column content">
{markdown_view!(
r#"
### About Me
Looks like you found my website. I suppose introductions are
in order.
My name's Tristan, I'm an avid Dutch-South African software
engineer. You probably either met me at an open source conference,
a hackathon, a badminton session or at a roleplaying table.
If not, well, this is also a great place to "meet" me. Have a
nosey!
### This Website
There is not a whole lot here at the moment.
You may find the following interesting though:
- A [little web app](~/src/music_sample.html) showing
off what WebGL can do in combination with the JavaScript
Audio interface."#
)}
</div>
<div class="column content">
{markdown_view!(
r#"### My Work
I'm interested in a variety of things in the open source
world. Perhaps thanks to my pursuit of the perfect Linux desktop,
this has revolved a lot around reproducible build and deployment
systems for the last few years, initially starting with
[BuildStream](https://buildstream.build/) back in ~2017. I gave a
couple of talks on it at build meetups in the UK in subsequent
years, though sadly most evidence of that appears to have
disappeared.
Since then this has culminated in a strong fondness for
[NixOS](https://nixos.org/) and Nix, as its active community makes
private use cases much more feasible. As such, I have a vested
interest in making this community as large as possible - I post a
lot on the NixOS [discourse](https://discourse.nixos.org/) trying
to help newcomers out where I can.
I also just enjoy Programming, my core languages for personal work
are currently probably Rust and Python, although I have a very
varied background. This is in part due to my former work as a
consultant, which required new languages every few months. I have
experience from JavaScript over Elm to Kotlin, but eventually I
hope I might only need to write Rust ;)
If you're interested in seeing these things for yourself,
visit my [Gitlab](https://gitlab.com/tlater) and
[GitHub](https://github.com/tlater) pages.
"#
)}
</div>
</div>
</div>
</section>
}
}

View file

@ -0,0 +1,9 @@
pub mod app;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::app::*;
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}

View file

@ -0,0 +1,38 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::logging::log;
use leptos::prelude::*;
use leptos_axum::{LeptosRoutes, generate_route_list};
use tlaternet_webserver::app::*;
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
let app = Router::new()
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View file

@ -0,0 +1,50 @@
@use "bulma/sass/utilities/initial-variables" as iv with (
$black: #0f0f0f,
$grey-darker: #11151c,
$grey-light: #dddddd,
$white: #ffffff,
$orange: #d26937,
$yellow: #b58900,
$green: #2aa889,
$cyan: #599cab,
$blue: #195466,
$red: #dc322f,
);
iv.$family-sans-serif: "Nunito", iv.$family-sans-serif;
iv.$family-monospace: "Hack", iv.$family-monospace;
@forward "bulma/sass/utilities/functions";
@use "bulma/sass/utilities/derived-variables" with (
$link: iv.$green,
$primary: #99d1ce,
);
@forward "bulma/sass/utilities/controls";
@forward "bulma/sass/base" with (
$body-background-color: iv.$black,
$body-color: iv.$grey-light,
$hr-background-color: iv.$grey-light,
$hr-height: 1px,
);
@forward "bulma/sass/themes";
@use "bulma/sass/elements/content" with (
$content-heading-weight: iv.$weight-semibold,
);
@use "bulma/sass/elements/title" with (
$title-color: #99d1ce,
);
@forward "bulma/sass/grid/columns";
@forward "bulma/sass/helpers/typography";
@forward "bulma/sass/helpers/color";
@forward "bulma/sass/layout/container";
@forward "bulma/sass/layout/section";
@forward "bulma/sass/components/navbar";

View file

@ -0,0 +1,48 @@
@use "@fontsource-utils/scss/src/mixins" as fontsource with (
$display: auto
);
@use "@fontsource-variable/arimo/scss/metadata.scss" as arimo;
@use "@fontsource-variable/nunito/scss/metadata.scss" as nunito;
@include fontsource.faces(
$metadata: nunito.$metadata,
$weights: (
300,
400,
500,
600,
700,
),
$subsets: latin,
$styles: (
normal,
italic,
),
$family: "Nunito",
$directory: "/@fontsource-variable/nunito"
);
@include fontsource.faces(
$metadata: arimo.$metadata,
$weights: 400,
$subsets: latin,
$styles: normal,
$family: "Arimo",
$directory: "/@fontsource-variable/arimo"
);
// Hack *does* come with its own CSS, but it's broken and hasn't seen
// a release since https://github.com/source-foundry/Hack/issues/467
// was resolved.
$variants: regular normal 400, bold normal 700, italic italic 400, bolditalic italic 700;
@each $name, $style, $weights in $variants {
@font-face {
font-family: "Hack";
font-style: $style;
font-display: auto;
font-weight: $weights;
src: url("/hack-font/hack-#{$name}-subset.woff2") format("woff2-variations");
}
}

View file

@ -0,0 +1,2 @@
@use "fonts";
@use "custom-bulma";

View file

@ -0,0 +1,83 @@
const self = "pkgs/packages/webserver/package.nix"
let tmpdir = mktemp -d webserver-update.XXXXXXXXXX
let dependencies = {
fontsource-scss: (prefetch-npm @fontsource-utils/scss)
fontsource-arimo: (prefetch-npm @fontsource-variable/arimo)
fontsource-nunito: (prefetch-npm @fontsource-variable/nunito)
bulma: (prefetch-github jgthms bulma)
hack-font: (prefetch-github source-foundry Hack)
}
$dependencies | items {|name, metadata| $metadata | update-dependency $name }
rm -r $tmpdir
def prefetch-npm [package: string] {
let metadata = http get $'https://registry.npmjs.org/($package)'
let version = $metadata.dist-tags.latest
let url = ($metadata.versions | get $version).dist.tarball
let tarball = ($tmpdir | path join "package.tgz")
http get $url | save -f $tarball
let hash = nix hash file $tarball
{
url: $url
version: $version
hash: $hash
}
}
def prefetch-github [owner: string, repo: string] {
let metadata = http get $'https://api.github.com/repos/($owner)/($repo)/releases/latest'
let prefetch = nix-prefetch-github --rev $metadata.tag_name --json $owner $repo | from json
$prefetch | select hash | insert version ($metadata.name | str trim --left --char v)
}
def update-dependency [dependency_name: string] {
const replace_attribute_template = "
id: update-attribute
language: nix
utils:
is-attribute:
kind: string_fragment
inside:
kind: binding
stopBy: end
has:
field: attrpath
regex: '{attribute}'
rule:
matches: is-attribute
not:
regex: '{replacement}'
inside:
kind: binding
stopBy: end
has:
field: attrpath
regex: '{dependency_name}'
fix: '{replacement}'"
let template_data = (
$in | if ($in has url) {
[{attribute: url replacement: $in.url}]
} else { [] }
) ++ [
{attribute: version replacement: $in.version}
{attribute: hash replacement: $in.hash}
];
let ast_grep_rule = (
$template_data
| each { $in | insert dependency_name $dependency_name | format pattern $replace_attribute_template }
| str join "\n---\n"
)
ast-grep scan --update-all --inline-rules $ast_grep_rule $self
}