wip: handle vite tooling manually

This commit is contained in:
Alexander Navarro 2025-04-25 16:53:44 -04:00
parent 9c1a8f030c
commit e8f111c2ff
11 changed files with 134 additions and 71 deletions

View file

@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run with vite" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="buildProfileId" value="dev" />
<option name="command" value="run --package compendium --bin compendium --features vite" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs>
<env name="CPD_DB_HOST" value="localhost" />
<env name="CPD_DB_NAME" value="compendium" />
<env name="CPD_DB_PASSWORD" value="password" />
<env name="CPD_DB_USER" value="postgres" />
</envs>
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>

View file

@ -1,10 +1,11 @@
[package] [package]
name = "compendium" name = "compendium"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[features] [features]
bundled = [] embed = ["vite"]
vite = []
[dependencies] [dependencies]
axum = { version = "0.8.1", features = ["macros"] } axum = { version = "0.8.1", features = ["macros"] }

View file

@ -1,24 +1,27 @@
use std::fs;
use std::path::Path;
fn main() { fn main() {
let out_dir = std::env::var("OUT_DIR").unwrap(); #[cfg(feature = "vite")]
// we only need to bundle the templates with the
// feature is enabled.
#[cfg(not(debug_assertions))]
{ {
let out_dir = if cfg!(feature = "embed") {
std::env::var("OUT_DIR").unwrap()
} else {
format!("../../{}", std::env::var("CPD_PUBLIC_DIR").unwrap_or(String::from("public")))
};
std::process::Command::new("bun") std::process::Command::new("bun")
.args(&["vite", "build", "--outDir", &out_dir]) .args(&["vite", "build", "--outDir", &out_dir])
.status() .status()
.unwrap(); .unwrap();
minijinja_embed::embed_templates!("frontend/templates");
}
#[cfg(debug_assertions)] println!("cargo::rerun-if-changed=frontend/assets");
println!("cargo::rerun-if-changed=frontend/static");
println!("cargo::rerun-if-env-changed=CPD_PUBLIC_DIR");
println!("cargo::rerun-if-env-changed=CARGO_FEATURE_EMBED");
}
println!("cargo::rerun-if-env-changed=CARGO_FEATURE_VITE");
#[cfg(feature = "embed")]
{ {
// dummy file to satisfy the compiler in dev builds minijinja_embed::embed_templates!("frontend/templates");
let dest_path = Path::new(&out_dir).join(".vite/manifest.json"); println!("cargo::rerun-if-changed=frontend/templates");
fs::create_dir_all(dest_path.parent().unwrap()).unwrap();
fs::write(&dest_path, "{}").unwrap();
} }
} }

0
frontend/static/.gitkeep Normal file
View file

View file

@ -8,8 +8,8 @@
{% if is_production == false %} {% if is_production == false %}
<script src="{{ asset('/@vite/client') }}" type="module"></script> <script src="{{ asset('/@vite/client') }}" type="module"></script>
{% endif %} {% endif %}
<link href="{{ asset('assets/css/style.scss') }}" rel="stylesheet"/> <link href="{{ asset('css/style.scss') }}" rel="stylesheet"/>
<script src="{{ asset('assets/js/index.ts') }}" type="module"></script> <script src="{{ asset('js/index.ts') }}" type="module"></script>
<title> <title>
{% block title %}Axum web service!{% endblock %} {% block title %}Axum web service!{% endblock %}
</title> </title>

View file

@ -84,6 +84,7 @@ impl DBConfig {
pub struct Config { pub struct Config {
pub db: DBConfig, pub db: DBConfig,
pub addr: SocketAddr, pub addr: SocketAddr,
pub public_dir: PathBuf,
} }
impl Default for Config { impl Default for Config {
@ -91,6 +92,7 @@ impl Default for Config {
Config { Config {
db: DBConfig::default(), db: DBConfig::default(),
addr: "0.0.0.0:3000".parse().unwrap(), addr: "0.0.0.0:3000".parse().unwrap(),
public_dir: PathBuf::from("public"),
} }
} }
} }

View file

@ -17,7 +17,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let config = Config::new("./config.toml".into())?; let config = Config::new("./config.toml".into())?;
let assets = Assets::new(); let assets = Assets::new(&config)?;
// Logs // Logs
tracing_subscriber::registry() tracing_subscriber::registry()
@ -40,7 +40,7 @@ async fn main() -> Result<()> {
let (tx_state, tx_layer) = Tx::setup(pool); let (tx_state, tx_layer) = Tx::setup(pool);
let app = router::new() let app = router::new(&config)
.layer(TraceLayer::new_for_http().on_request(())) .layer(TraceLayer::new_for_http().on_request(()))
.layer(tx_layer) .layer(tx_layer)
.layer(AutoVaryLayer) .layer(AutoVaryLayer)

View file

@ -1,6 +1,8 @@
use crate::config::Config;
use crate::{AppState, ResultTemplate, Tx};
use axum::{ use axum::{
extract::{Path, State}, extract::State
http::{header, HeaderMap, HeaderValue}, ,
routing::get, routing::get,
Router, Router,
}; };
@ -9,34 +11,38 @@ use chrono::Utc;
use minijinja::context; use minijinja::context;
use serde::Serialize; use serde::Serialize;
use sqlx::prelude::FromRow; use sqlx::prelude::FromRow;
use tower_http::services::ServeDir;
use crate::static_assets::{ViteManifest, VITE_MANIFEST_STR}; pub fn new(config: &Config) -> Router<AppState> {
use crate::{AppState, Result, ResultTemplate, Tx}; let router = Router::new()
pub fn new() -> Router<AppState> {
Router::new()
.route("/", get(handler_home)) .route("/", get(handler_home))
.route("/assets/{*asset}", get(handle_assets)) .nest("/public", load_asset_router(config));
router
} }
async fn handle_assets( #[cfg(feature = "embed")]
State(state): State<AppState>, fn load_asset_router(config: &Config) -> Router<AppState> {
Path(asset_path): Path<String>, Router::new()
) -> Result<(HeaderMap, String)> { .fallback_service(
let mut headers = HeaderMap::new(); ServeDir::new(&config.public_dir),
)
.nest_service(
"/assets",
ServeDir::new(concat!(env!("OUT_DIR"), "/assets")),
)
}
let manifest: ViteManifest = serde_json::from_str(VITE_MANIFEST_STR)?; #[cfg(not(feature = "embed"))]
fn load_asset_router(config: &Config) -> Result<(ViteManifest, String)> {
headers.insert( Router::new()
header::CONTENT_TYPE, .fallback_service(
HeaderValue::from_str("text/plain").unwrap(), ServeDir::new(&config.public_dir),
); )
.nest_service(
println!("{}, {:?}", asset_path, manifest); "/assets",
let asset = manifest ServeDir::new(&config.public_dir.join("assets")),
.get(format!("frontend/assets/{}", asset_path).as_str()) )
.unwrap();
Ok((headers, asset.file.clone()))
} }
#[derive(FromRow, Debug, Serialize)] #[derive(FromRow, Debug, Serialize)]

View file

@ -1,17 +1,17 @@
mod template_functions; mod template_functions;
use crate::config::Config;
use crate::static_assets::template_functions::load_functions; use crate::static_assets::template_functions::load_functions;
use crate::ResultTemplate; use crate::{Result, ResultTemplate};
use axum::response::Html; use axum::response::Html;
use minijinja::{path_loader, Environment}; use minijinja::{path_loader, Environment};
use minijinja_autoreload::AutoReloader; use minijinja_autoreload::AutoReloader;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
pub const VITE_MANIFEST_STR: &str = include_str!(concat!(env!("OUT_DIR"), "/.vite/manifest.json"));
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[derive(Clone)]
pub struct ViteAsset { pub struct ViteAsset {
pub file: String, pub file: String,
pub src: String, pub src: String,
@ -22,6 +22,7 @@ pub type ViteManifest = HashMap<String, ViteAsset>;
pub struct Assets { pub struct Assets {
templates: Environment<'static>, templates: Environment<'static>,
manifest: ViteManifest,
_reloader: Option<AutoReloader>, _reloader: Option<AutoReloader>,
} }
@ -29,14 +30,19 @@ impl Clone for Assets {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
templates: self.templates.clone(), templates: self.templates.clone(),
manifest: self.manifest.clone(),
_reloader: None, _reloader: None,
} }
} }
} }
impl Assets { impl Assets {
pub fn new() -> Self { pub fn new(config: &Config) -> Result<Self> {
let (manifest, manifest_str) = Self::load_vite_manifest(config)?;
let mut templates = Environment::new(); let mut templates = Environment::new();
templates.add_global("vite_manifest", manifest_str);
load_functions(&mut templates); load_functions(&mut templates);
templates.set_loader(path_loader("frontend/templates")); templates.set_loader(path_loader("frontend/templates"));
let mut _reloader = None; let mut _reloader = None;
@ -60,10 +66,27 @@ impl Assets {
minijinja_embed::load_templates!(&mut templates); minijinja_embed::load_templates!(&mut templates);
} }
Self { Ok(Self {
templates, templates,
manifest,
_reloader, _reloader,
} })
}
#[cfg(feature = "embed")]
fn load_vite_manifest(_config: &Config) -> Result<(ViteManifest, String)> {
let file = include_str!(concat!(env!("OUT_DIR"), "/.vite/manifest.json"));
Ok((serde_json::from_str(file)?, file.to_string()))
}
#[cfg(not(feature = "embed"))]
fn load_vite_manifest(config: &Config) -> Result<(ViteManifest, String)> {
let file = std::fs::read_to_string(config.public_dir.join(".vite/manifest.json"))?;
Ok((serde_json::from_str(file.as_str())?, file))
}
pub fn get_vite_asset(&self, asset: &str) -> Option<&ViteAsset> {
self.manifest.get(asset)
} }
pub fn render_template<S: Serialize>(&self, path: &str, ctx: S) -> ResultTemplate { pub fn render_template<S: Serialize>(&self, path: &str, ctx: S) -> ResultTemplate {

View file

@ -1,27 +1,24 @@
use minijinja::{Environment, Error, ErrorKind}; use crate::static_assets::ViteManifest;
use minijinja::{Environment, Error, ErrorKind, State};
use serde_json::from_str;
fn load_asset(asset: String) -> Result<String, Error> {
#[cfg(debug_assertions)]
{
let url =
url::Url::parse("http://localhost:3001/frontend/").map_err(|_| ErrorKind::EvalBlock)?;
return Ok(url fn load_asset(state: &State, asset: String) -> Result<String, Error> {
.join(asset.as_str()) let manifest: ViteManifest = from_str(
.map_err(|_| ErrorKind::EvalBlock)? state.lookup("vite_manifest")
.to_string()); .ok_or(ErrorKind::EvalBlock)?
} .as_str()
.ok_or(ErrorKind::EvalBlock)?
).map_err(|_| ErrorKind::EvalBlock)?;
#[cfg(not(debug_assertions))] let local_asset = manifest.get(asset.as_str());
{
let url = url::Url::parse("http://localhost:3001").map_err(|_| ErrorKind::EvalBlock)?;
return Ok(url Ok(match local_asset {
.join(asset.as_str()) None => asset,
.map_err(|_| ErrorKind::EvalBlock)? Some(asset) => {
.path() format!("/public/{}", asset.file.clone())
.to_string()); }
} })
} }
pub(super) fn load_functions(env: &mut Environment) { pub(super) fn load_functions(env: &mut Environment) {

View file

@ -1,6 +1,9 @@
import {defineConfig} from "vite"; import {defineConfig} from "vite";
export default defineConfig({ export default defineConfig({
root: "frontend/assets",
publicDir: "frontend/static",
base: "/public/assets",
plugins: [], plugins: [],
server: { server: {
port: 3001, port: 3001,
@ -8,6 +11,9 @@ export default defineConfig({
cors: true, cors: true,
}, },
build: { build: {
outDir: "../../public", // outDir is relative to the root config
assetsDir: "assets",
emptyOutDir: true,
manifest: true, manifest: true,
rollupOptions: { rollupOptions: {
input: [ input: [