Skip to content

Commit

Permalink
feat: Add support for the tera templating engine
Browse files Browse the repository at this point in the history
- Use the tera templating engine for rendering inital page loads

- Use the `in-vite` crate to implement the required integration with
vite rather than the internal implementation.

- Implement default initializer to be used with loco apps
  • Loading branch information
mstallmo committed Sep 26, 2024
1 parent d674577 commit 0e3ad49
Show file tree
Hide file tree
Showing 11 changed files with 4,563 additions and 1,076 deletions.
4,793 changes: 4,251 additions & 542 deletions Cargo.lock

Large diffs are not rendered by default.

21 changes: 15 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,28 @@ repository = "https://github.com/mstallmo/inertia-loco"
keywords = ["loco", "inertia"]

[dependencies]
axum = "0.7.5"
anyhow = "1.0.89"
async-trait = "0.1.74"
axum = "0.7.5"
hex = "0.4.3"
http = "1.0.0"
hyper = "1.0.1"
# in-vite = "0.1.3"
in-vite = { git = "https://github.com/mstallmo/in-vite", branch = "react" }
loco-rs = "0.8.1"
maud = "0.25.0"
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"
indoc = "2.0.4"
sha1 = "0.10.6"
hex = "0.4.3"
maud = "0.25.0"
tera = "1.20.0"
tower-http = { version = "0.5.0", features = ["set-header", "trace"] }
tracing = "0.1.40"

[features]
default = ["tera"]
tera = ["in-vite/tera"]

[dev-dependencies]
http-body-util = "0.1.0"
reqwest = "0.11.22"
tokio = { version = "1.34.0", features = ["full"] }
tower-http = { version = "0.5.0", features = ["set-header", "trace"] }
http-body-util = "0.1.0"
163 changes: 154 additions & 9 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
use std::sync::Arc;
use crate::tera::InertiaRootTag;
use anyhow::{anyhow, Result};
use hex::encode;
use in_vite::{Vite, ViteOptions};
use loco_rs::environment::Environment;
use serde_json::to_value;
use sha1::{Digest, Sha1};
use std::{
fs::read,
path::{Path, PathBuf},
sync::Arc,
};
use tera::Tera;

const VIEWS_DIR: &str = "assets/views";

struct Inner {
version: Option<String>,
layout: Box<dyn Fn(String) -> String + Send + Sync>,
tera: tera::Tera,
application_layout: String,
}

#[derive(Clone)]
Expand All @@ -17,22 +32,152 @@ impl InertiaConfig {
/// page load. See the [crate::vite] module for an implementation
/// of this for vite.
pub fn new(
views_dir: PathBuf,
vite_manifest_path: PathBuf,
application_layout: String,
version: Option<String>,
layout: Box<dyn Fn(String) -> String + Send + Sync>,
) -> InertiaConfig {
let inner = Inner { version, layout };
InertiaConfig {
) -> Result<InertiaConfig> {
let mut tera = Self::init_tera(&views_dir)?;
Self::init_vite(&mut tera, &vite_manifest_path);
Self::register_inertia_root(&mut tera);

let inner = Inner {
version,
tera,
application_layout,
};

Ok(InertiaConfig {
inner: Arc::new(inner),
})
}

fn init_tera(views_dir: &Path) -> Result<Tera> {
if !views_dir.exists() {
return Err(anyhow!(
"missing views directory: `{}`",
views_dir.display()
));
}

let tera = Tera::new(
views_dir
.join("**")
.join("*.html")
.to_str()
.ok_or_else(|| anyhow!("invalid blob"))?,
)?;

Ok(tera)
}

fn init_vite(tera: &mut Tera, manifest_path: &Path) {
let opts = ViteOptions::default().manifest_path(
manifest_path
.to_str()
.expect("Failed to convert path to str"),
);
let vite = Vite::with_options(opts);
tera.register_function("vite", vite);
}

fn register_inertia_root(tera: &mut Tera) {
let inertia_root_tag = InertiaRootTag {};
tera.register_function("inertia_root", inertia_root_tag);
}

/// Returns a cloned optional version string.
pub fn version(&self) -> Option<String> {
self.inner.version.clone()
}

/// Returns a reference to the layout function.
pub fn layout(&self) -> &(dyn Fn(String) -> String + Send + Sync) {
&self.inner.layout
/// Returns the rendered application layout.
pub fn layout<S: serde::Serialize + Clone>(&self, props: S) -> Result<String> {
let mut context = tera::Context::new();
context.insert("props", &to_value(props)?);

let renderd_html = self
.inner
.tera
.render(&self.inner.application_layout, &context)?;
Ok(renderd_html)
}
}

pub struct InertiaConfigBuilder {
environment: Environment,
views_dir: PathBuf,
application_layout: String,
vite_manifest_path: PathBuf,
}

impl InertiaConfigBuilder {
/// Creates a new instance with common Loco defaults set
/// views_dir: assets/views
/// application_layou: layout.html
/// vite_manifest_path: frontend/dist/.vite/manifest.json
pub fn new(environment: Environment) -> Self {
InertiaConfigBuilder {
environment,
views_dir: PathBuf::from(VIEWS_DIR),
application_layout: "layout.html".to_string(),
vite_manifest_path: PathBuf::from("frontend/dist/.vite/manifest.json"),
}
}

/// Sets the environment that [InertiaConfig] should be built for
pub fn environment(mut self, environment: Environment) -> Self {
self.environment = environment;
self
}

/// Sets the directory to render view templates from.
pub fn views_dir<S: AsRef<str>>(mut self, views_dir: &S) -> Self {
self.views_dir = PathBuf::from(views_dir.as_ref());
self
}

/// Sets the template file to use as the application layout. This is the template
/// where javascript modules and the inertia root will be placed into.
pub fn application_layout<S: AsRef<str>>(mut self, application_layout: &S) -> Self {
self.application_layout = application_layout.as_ref().to_string();
self
}

/// Sets the path to vite manifest.json file
pub fn vite_manifest_path<S: AsRef<str>>(mut self, manifest_path: &S) -> Self {
self.vite_manifest_path = PathBuf::from(manifest_path.as_ref());
self
}

/// Builds a new instance of [InertiaConfig]
pub fn build(self) -> Result<InertiaConfig> {
match self.environment {
Environment::Development => InertiaConfig::new(
self.views_dir,
self.vite_manifest_path,
self.application_layout,
None,
),
_ => {
let version = self.hash_manifest()?;
InertiaConfig::new(
self.views_dir,
self.vite_manifest_path,
self.application_layout,
Some(version),
)
}
}
}
}

impl InertiaConfigBuilder {
fn hash_manifest(&self) -> Result<String> {
let manifest_bytes = read(&self.vite_manifest_path)?;
let mut hasher = Sha1::new();
hasher.update(manifest_bytes);
let result = hasher.finalize();
Ok(encode(result))
}
}
32 changes: 32 additions & 0 deletions src/initializer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! Default Loco Initializer provided for adding InertiaJS rendering to Loco apps
//!
//! If modifications are needed to the behavior of the initializer a custom initializer
//! can be implemented in your Loco app directly.
use crate::InertiaConfigBuilder;
use axum::{async_trait, Extension, Router as AxumRouter};
use loco_rs::{
app::{AppContext, Initializer},
Error, Result,
};
use tracing::debug;

pub struct InertiaInitializer;

#[async_trait]
impl Initializer for InertiaInitializer {
fn name(&self) -> String {
"inertia".to_string()
}

/// Creates a new [InertiaConfig] instance based on the [AppContext] environment and adds
/// it to the router as a layer [Extension]
async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result<AxumRouter> {
debug!("Initializing...");

let inertia_config = InertiaConfigBuilder::new(ctx.environment.clone())
.build()
.map_err(|err| Error::Message(err.to_string()))?;

Ok(router.layer(Extension(inertia_config)))
}
}
Loading

0 comments on commit 0e3ad49

Please sign in to comment.