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]
name = "compendium"
version = "0.1.0"
edition = "2021"
edition = "2024"
[features]
bundled = []
embed = ["vite"]
vite = []
[dependencies]
axum = { version = "0.8.1", features = ["macros"] }

View file

@ -1,24 +1,27 @@
use std::fs;
use std::path::Path;
fn main() {
let out_dir = std::env::var("OUT_DIR").unwrap();
// we only need to bundle the templates with the
// feature is enabled.
#[cfg(not(debug_assertions))]
#[cfg(feature = "vite")]
{
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")
.args(&["vite", "build", "--outDir", &out_dir])
.status()
.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
let dest_path = Path::new(&out_dir).join(".vite/manifest.json");
fs::create_dir_all(dest_path.parent().unwrap()).unwrap();
fs::write(&dest_path, "{}").unwrap();
minijinja_embed::embed_templates!("frontend/templates");
println!("cargo::rerun-if-changed=frontend/templates");
}
}

0
frontend/static/.gitkeep Normal file
View file

View file

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

View file

@ -84,6 +84,7 @@ impl DBConfig {
pub struct Config {
pub db: DBConfig,
pub addr: SocketAddr,
pub public_dir: PathBuf,
}
impl Default for Config {
@ -91,6 +92,7 @@ impl Default for Config {
Config {
db: DBConfig::default(),
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]
async fn main() -> Result<()> {
let config = Config::new("./config.toml".into())?;
let assets = Assets::new();
let assets = Assets::new(&config)?;
// Logs
tracing_subscriber::registry()
@ -40,7 +40,7 @@ async fn main() -> Result<()> {
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(tx_layer)
.layer(AutoVaryLayer)

View file

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

View file

@ -1,17 +1,17 @@
mod template_functions;
use crate::config::Config;
use crate::static_assets::template_functions::load_functions;
use crate::ResultTemplate;
use crate::{Result, ResultTemplate};
use axum::response::Html;
use minijinja::{path_loader, Environment};
use minijinja_autoreload::AutoReloader;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const VITE_MANIFEST_STR: &str = include_str!(concat!(env!("OUT_DIR"), "/.vite/manifest.json"));
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[derive(Clone)]
pub struct ViteAsset {
pub file: String,
pub src: String,
@ -22,6 +22,7 @@ pub type ViteManifest = HashMap<String, ViteAsset>;
pub struct Assets {
templates: Environment<'static>,
manifest: ViteManifest,
_reloader: Option<AutoReloader>,
}
@ -29,14 +30,19 @@ impl Clone for Assets {
fn clone(&self) -> Self {
Self {
templates: self.templates.clone(),
manifest: self.manifest.clone(),
_reloader: None,
}
}
}
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();
templates.add_global("vite_manifest", manifest_str);
load_functions(&mut templates);
templates.set_loader(path_loader("frontend/templates"));
let mut _reloader = None;
@ -60,10 +66,27 @@ impl Assets {
minijinja_embed::load_templates!(&mut templates);
}
Self {
Ok(Self {
templates,
manifest,
_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 {

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

View file

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