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 50084c76f1
Signed by: tlater
GPG key ID: 02E935006CF2E8E7
15 changed files with 3128 additions and 165 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/

2610
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"
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"
assets-dir = "public"
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,184 @@
{
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
];
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=";
};
installPhase = ''
mkdir -p $out/node_modules/@fontsource-variable/nunito/
cp -r . $out/node_modules/@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=";
};
installPhase = ''
mkdir -p $out/node_modules/@fontsource-variable/arimo/
cp -r . $out/node_modules/@fontsource-variable/arimo/
'';
};
};
# 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 = [
dependencies.bulma
dependencies.fontsource-scss
dependencies.fontsource-arimo
dependencies.fontsource-nunito
];
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 = {
inherit 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
];
};
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,71 @@
use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{
components::{Route, Router, Routes},
StaticSegment,
};
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" contnet="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 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" />
// content for this welcome page
<Router>
<main>
<Routes fallback=|| "Page not found.".into_view()>
<Route path=StaticSegment("") view=HomePage />
</Routes>
</main>
</Router>
}
}
/// Renders the home page of your application.
#[component]
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">tlater.net</span>
</h1>
<hr />
<div class="columns">
<div class="column content" />
<div class="column content" />
</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,39 @@
@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";
@forward "bulma/sass/utilities/derived-variables";
@forward "bulma/sass/utilities/controls";
@forward "bulma/sass/base";
@forward "bulma/sass/themes";
@forward "bulma/sass/elements/content";
@forward "bulma/sass/elements/title";
@forward "bulma/sass/grid/columns";
@forward "bulma/sass/helpers/typography";
@forward "bulma/sass/layout/container";
@forward "bulma/sass/layout/section";
@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;

View file

@ -0,0 +1,80 @@
let 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)
}
$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
}
def update-dependency [dependency_name: string] {
let 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
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
}