Skip to content

Commit

Permalink
feat: ✨ added support for static content and aliases
Browse files Browse the repository at this point in the history
BREAKING CHANGE: actix web integration now takes `static_dirs` and `static_aliases` options
  • Loading branch information
arctic-hen7 committed Sep 19, 2021
1 parent 6cfe8e1 commit 7f38ea7
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 16 deletions.
1 change: 1 addition & 0 deletions examples/basic/static/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file is accessible at `/.perseus/static/test.txt`!
16 changes: 14 additions & 2 deletions examples/cli/.perseus/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use actix_web::{App, HttpServer};
use app::{
get_config_manager, get_error_pages, get_locales, get_templates_map, get_translations_manager,
APP_ROOT,
get_config_manager, get_error_pages, get_locales, get_static_aliases, get_templates_map,
get_translations_manager, APP_ROOT,
};
use futures::executor::block_on;
use perseus_actix_web::{configurer, Options};
use std::collections::HashMap;
use std::env;
use std::fs;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
Expand All @@ -30,6 +32,16 @@ async fn main() -> std::io::Result<()> {
root_id: APP_ROOT.to_string(),
snippets: "dist/pkg/snippets".to_string(),
error_pages: get_error_pages(),
// The CLI supports static content in `../static` by default if it exists
// This will be available directly at `/.perseus/static`
static_dirs: if fs::metadata("../static").is_ok() {
let mut static_dirs = HashMap::new();
static_dirs.insert("".to_string(), "../static".to_string());
static_dirs
} else {
HashMap::new()
},
static_aliases: get_static_aliases(),
},
get_config_manager(),
block_on(get_translations_manager()),
Expand Down
5 changes: 4 additions & 1 deletion examples/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ define_app! {
crate::pages::index::get_page::<G>(),
crate::pages::about::get_page::<G>()
],
error_pages: crate::error_pages::get_error_pages()
error_pages: crate::error_pages::get_error_pages(),
static_aliases: {
"/test.txt" => "static/test.txt"
}
}
37 changes: 32 additions & 5 deletions packages/perseus-actix-web/src/configurer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::initial_load::initial_load;
use crate::page_data::page_data;
use crate::translations::translations;
use actix_files::{Files, NamedFile};
use actix_web::web;
use actix_web::{web, HttpRequest};
use perseus::{
get_render_cfg, ConfigManager, ErrorPages, Locales, SsrNode, TemplateMap, TranslationsManager,
};
Expand Down Expand Up @@ -30,6 +30,13 @@ pub struct Options {
pub snippets: String,
/// The error pages for the app. These will be server-rendered if an initial load fails.
pub error_pages: ErrorPages<SsrNode>,
/// Directories to serve static content from, mapping URL to folder path. Note that the URL provided will be gated behind
/// `.perseus/static/`, and must have a leading `/`. If you're using a CMS instead, you should set these up outside the Perseus
/// server (but they might still be on the same machine, you can still add more routes after Perseus is configured).
pub static_dirs: HashMap<String, String>,
/// A map of URLs to act as aliases for certain static resources. These are particularly designed for things like a site manifest or
/// favicons, which should be stored in a static directory, but need to be aliased at a path like `/favicon.ico`.
pub static_aliases: HashMap<String, String>,
}

async fn render_conf(
Expand All @@ -43,8 +50,18 @@ async fn js_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
async fn wasm_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.wasm_bundle)
}
async fn static_alias(opts: web::Data<Options>, req: HttpRequest) -> std::io::Result<NamedFile> {
let filename = opts.static_aliases.get(req.path());
let filename = match filename {
Some(filename) => filename,
// If the path doesn't exist, then the alias is not found
None => return Err(std::io::Error::from(std::io::ErrorKind::NotFound)),
};
NamedFile::open(filename)
}

/// Configures an existing Actix Web app for Perseus. This returns a function that does the configuring so it can take arguments.
/// Configures an existing Actix Web app for Perseus. This returns a function that does the configuring so it can take arguments. This
/// includes a complete wildcard handler (`*`), and so it should be configured after any other routes on your server.
pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'static>(
opts: Options,
config_manager: C,
Expand Down Expand Up @@ -104,8 +121,18 @@ pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'st
)
// This allows gettting JS interop snippets (including ones that are supposedly 'inlined')
// These won't change, so they can be set as a filesystem dependency safely
.service(Files::new("/.perseus/snippets", &opts.snippets))
// For everything else, we'll serve the app shell directly
.route("*", web::get().to(initial_load::<C, T>));
.service(Files::new("/.perseus/snippets", &opts.snippets));
// Now we add support for any static content the user wants to provide
for (url, static_dir) in opts.static_dirs.iter() {
cfg.service(Files::new(&format!("/.perseus/static{}", url), static_dir));
}
// And finally add in aliases for static content as necessary
for (url, _static_path) in opts.static_aliases.iter() {
// This handler indexes the path of the request in `opts.static_aliases` to figure out what to serve
cfg.route(url, web::get().to(static_alias));
}
// For everything else, we'll serve the app shell directly
// This has to be done AFTER everything else, because it will match anything that's left
cfg.route("*", web::get().to(initial_load::<C, T>));
}
}
70 changes: 62 additions & 8 deletions packages/perseus/src/macros.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// TODO parse `no_i18n` properly so the user can specify `false`

/// An internal macro used for defining a function to get the user's preferred config manager (which requires multiple branches).
#[macro_export]
macro_rules! define_get_config_manager {
Expand Down Expand Up @@ -99,15 +97,49 @@ macro_rules! define_get_locales {
};
}

/// An internal macro for defining a function that gets the user's static content aliases (abstracted because it needs multiple
/// branches).
#[macro_export]
macro_rules! define_get_static_aliases {
(
static_aliases: {
$($url:literal => $resource:literal)*
}
) => {
pub fn get_static_aliases() -> ::std::collections::HashMap<String, String> {
let mut static_aliases = ::std::collections::HashMap::new();
$(
let resource = $resource.to_string();
// We need to move this from being scoped to the app to being scoped for `.perseus/`
// TODO make sure this works properly on Windows
let resource = if resource.starts_with("/") {
// Absolute paths should be left as is
resource
} else if resource.starts_with("./") {
// `./` -> `../`
format!(".{}", resource)
} else {
// Anything else (including `../`) gets a `../` prepended
format!("../{}", resource)
};
static_aliases.insert($url.to_string(), resource);
)*
static_aliases
}
};
() => {
pub fn get_static_aliases() -> ::std::collections::HashMap<String, String> {
::std::collections::HashMap::new()
}
};
}

/// Defines the components to create an entrypoint for the app. The actual entrypoint is created in the `.perseus/` crate (where we can
/// get all the dependencies without driving the user's `Cargo.toml` nuts). This also defines the template map. This is intended to make
/// compatibility with the Perseus CLI significantly easier. Perseus makes i18n opt-out, so if you don't intend to use it, set `no_i18n`
/// to `true` in `locales`. Note that you must still specify a default locale for verbosity and correctness. If you specify `no_i18n` and
/// a custom translations manager, the latter will override.
/// compatibility with the Perseus CLI significantly easier.
///
/// Warning: all properties must currently be in the correct order (`root`, `templates`, `error_pages`, `locales`, `config_manager`,
/// `translations_manager`).
// TODO make this syntax even more compact and beautiful? (error pages inside templates?)
/// Warning: all properties must currently be in the correct order (`root`, `templates`, `error_pages`, `locales`, `static_aliases`,
/// `config_manager`, `translations_manager`).
#[macro_export]
macro_rules! define_app {
// With locales
Expand All @@ -124,6 +156,9 @@ macro_rules! define_app {
other: [$($other_locale:literal),*]
$(,no_i18n: $no_i18n:literal)?
}
$(,static_aliases: {
$($url:literal => $resource:literal)*
})?
$(,config_manager: $config_manager:expr)?
$(,translations_manager: $translations_manager:expr)?
} => {
Expand All @@ -140,6 +175,9 @@ macro_rules! define_app {
// The user doesn't have to define any other locales (but they'll still get locale detection and the like)
other: [$($other_locale),*]
}
$(,static_aliases: {
$($url => $resource)*
})?
$(,config_manager: $config_manager)?
$(,translations_manager: $translations_manager)?
}
Expand All @@ -152,6 +190,9 @@ macro_rules! define_app {
$($template:expr),+
],
error_pages: $error_pages:expr
$(,static_aliases: {
$($url:literal => $resource:literal)*
})?
$(,config_manager: $config_manager:expr)?
$(,translations_manager: $translations_manager:expr)?
} => {
Expand All @@ -169,6 +210,9 @@ macro_rules! define_app {
other: [],
no_i18n: true
}
$(,static_aliases: {
$($url => $resource)*
})?
$(,config_manager: $config_manager)?
$(,translations_manager: $translations_manager)?
}
Expand All @@ -191,6 +235,9 @@ macro_rules! define_app {
// If this is defined at all, i18n will be disabled and the default locale will be set to `xx-XX`
$(,no_i18n: $no_i18n:literal)?
}
$(,static_aliases: {
$($url:literal => $resource:literal)*
})?
$(,config_manager: $config_manager:expr)?
$(,translations_manager: $translations_manager:expr)?
}
Expand Down Expand Up @@ -235,5 +282,12 @@ macro_rules! define_app {
pub fn get_error_pages<G: $crate::GenericNode>() -> $crate::ErrorPages<G> {
$error_pages
}

/// Gets any static content aliases provided by the user.
$crate::define_get_static_aliases!(
$(static_aliases: {
$($url => $resource)*
})?
);
};
}

0 comments on commit 7f38ea7

Please sign in to comment.