Skip to content

Commit 7f38ea7

Browse files
committed
feat: ✨ added support for static content and aliases
BREAKING CHANGE: actix web integration now takes `static_dirs` and `static_aliases` options
1 parent 6cfe8e1 commit 7f38ea7

File tree

5 files changed

+113
-16
lines changed

5 files changed

+113
-16
lines changed

examples/basic/static/test.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This file is accessible at `/.perseus/static/test.txt`!

examples/cli/.perseus/server/src/main.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use actix_web::{App, HttpServer};
22
use app::{
3-
get_config_manager, get_error_pages, get_locales, get_templates_map, get_translations_manager,
4-
APP_ROOT,
3+
get_config_manager, get_error_pages, get_locales, get_static_aliases, get_templates_map,
4+
get_translations_manager, APP_ROOT,
55
};
66
use futures::executor::block_on;
77
use perseus_actix_web::{configurer, Options};
8+
use std::collections::HashMap;
89
use std::env;
10+
use std::fs;
911

1012
#[actix_web::main]
1113
async fn main() -> std::io::Result<()> {
@@ -30,6 +32,16 @@ async fn main() -> std::io::Result<()> {
3032
root_id: APP_ROOT.to_string(),
3133
snippets: "dist/pkg/snippets".to_string(),
3234
error_pages: get_error_pages(),
35+
// The CLI supports static content in `../static` by default if it exists
36+
// This will be available directly at `/.perseus/static`
37+
static_dirs: if fs::metadata("../static").is_ok() {
38+
let mut static_dirs = HashMap::new();
39+
static_dirs.insert("".to_string(), "../static".to_string());
40+
static_dirs
41+
} else {
42+
HashMap::new()
43+
},
44+
static_aliases: get_static_aliases(),
3345
},
3446
get_config_manager(),
3547
block_on(get_translations_manager()),

examples/cli/src/lib.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ define_app! {
88
crate::pages::index::get_page::<G>(),
99
crate::pages::about::get_page::<G>()
1010
],
11-
error_pages: crate::error_pages::get_error_pages()
11+
error_pages: crate::error_pages::get_error_pages(),
12+
static_aliases: {
13+
"/test.txt" => "static/test.txt"
14+
}
1215
}

packages/perseus-actix-web/src/configurer.rs

+32-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::initial_load::initial_load;
22
use crate::page_data::page_data;
33
use crate::translations::translations;
44
use actix_files::{Files, NamedFile};
5-
use actix_web::web;
5+
use actix_web::{web, HttpRequest};
66
use perseus::{
77
get_render_cfg, ConfigManager, ErrorPages, Locales, SsrNode, TemplateMap, TranslationsManager,
88
};
@@ -30,6 +30,13 @@ pub struct Options {
3030
pub snippets: String,
3131
/// The error pages for the app. These will be server-rendered if an initial load fails.
3232
pub error_pages: ErrorPages<SsrNode>,
33+
/// Directories to serve static content from, mapping URL to folder path. Note that the URL provided will be gated behind
34+
/// `.perseus/static/`, and must have a leading `/`. If you're using a CMS instead, you should set these up outside the Perseus
35+
/// server (but they might still be on the same machine, you can still add more routes after Perseus is configured).
36+
pub static_dirs: HashMap<String, String>,
37+
/// A map of URLs to act as aliases for certain static resources. These are particularly designed for things like a site manifest or
38+
/// favicons, which should be stored in a static directory, but need to be aliased at a path like `/favicon.ico`.
39+
pub static_aliases: HashMap<String, String>,
3340
}
3441

3542
async fn render_conf(
@@ -43,8 +50,18 @@ async fn js_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
4350
async fn wasm_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
4451
NamedFile::open(&opts.wasm_bundle)
4552
}
53+
async fn static_alias(opts: web::Data<Options>, req: HttpRequest) -> std::io::Result<NamedFile> {
54+
let filename = opts.static_aliases.get(req.path());
55+
let filename = match filename {
56+
Some(filename) => filename,
57+
// If the path doesn't exist, then the alias is not found
58+
None => return Err(std::io::Error::from(std::io::ErrorKind::NotFound)),
59+
};
60+
NamedFile::open(filename)
61+
}
4662

47-
/// Configures an existing Actix Web app for Perseus. This returns a function that does the configuring so it can take arguments.
63+
/// Configures an existing Actix Web app for Perseus. This returns a function that does the configuring so it can take arguments. This
64+
/// includes a complete wildcard handler (`*`), and so it should be configured after any other routes on your server.
4865
pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'static>(
4966
opts: Options,
5067
config_manager: C,
@@ -104,8 +121,18 @@ pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'st
104121
)
105122
// This allows gettting JS interop snippets (including ones that are supposedly 'inlined')
106123
// These won't change, so they can be set as a filesystem dependency safely
107-
.service(Files::new("/.perseus/snippets", &opts.snippets))
108-
// For everything else, we'll serve the app shell directly
109-
.route("*", web::get().to(initial_load::<C, T>));
124+
.service(Files::new("/.perseus/snippets", &opts.snippets));
125+
// Now we add support for any static content the user wants to provide
126+
for (url, static_dir) in opts.static_dirs.iter() {
127+
cfg.service(Files::new(&format!("/.perseus/static{}", url), static_dir));
128+
}
129+
// And finally add in aliases for static content as necessary
130+
for (url, _static_path) in opts.static_aliases.iter() {
131+
// This handler indexes the path of the request in `opts.static_aliases` to figure out what to serve
132+
cfg.route(url, web::get().to(static_alias));
133+
}
134+
// For everything else, we'll serve the app shell directly
135+
// This has to be done AFTER everything else, because it will match anything that's left
136+
cfg.route("*", web::get().to(initial_load::<C, T>));
110137
}
111138
}

packages/perseus/src/macros.rs

+62-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// TODO parse `no_i18n` properly so the user can specify `false`
2-
31
/// An internal macro used for defining a function to get the user's preferred config manager (which requires multiple branches).
42
#[macro_export]
53
macro_rules! define_get_config_manager {
@@ -99,15 +97,49 @@ macro_rules! define_get_locales {
9997
};
10098
}
10199

100+
/// An internal macro for defining a function that gets the user's static content aliases (abstracted because it needs multiple
101+
/// branches).
102+
#[macro_export]
103+
macro_rules! define_get_static_aliases {
104+
(
105+
static_aliases: {
106+
$($url:literal => $resource:literal)*
107+
}
108+
) => {
109+
pub fn get_static_aliases() -> ::std::collections::HashMap<String, String> {
110+
let mut static_aliases = ::std::collections::HashMap::new();
111+
$(
112+
let resource = $resource.to_string();
113+
// We need to move this from being scoped to the app to being scoped for `.perseus/`
114+
// TODO make sure this works properly on Windows
115+
let resource = if resource.starts_with("/") {
116+
// Absolute paths should be left as is
117+
resource
118+
} else if resource.starts_with("./") {
119+
// `./` -> `../`
120+
format!(".{}", resource)
121+
} else {
122+
// Anything else (including `../`) gets a `../` prepended
123+
format!("../{}", resource)
124+
};
125+
static_aliases.insert($url.to_string(), resource);
126+
)*
127+
static_aliases
128+
}
129+
};
130+
() => {
131+
pub fn get_static_aliases() -> ::std::collections::HashMap<String, String> {
132+
::std::collections::HashMap::new()
133+
}
134+
};
135+
}
136+
102137
/// Defines the components to create an entrypoint for the app. The actual entrypoint is created in the `.perseus/` crate (where we can
103138
/// get all the dependencies without driving the user's `Cargo.toml` nuts). This also defines the template map. This is intended to make
104-
/// compatibility with the Perseus CLI significantly easier. Perseus makes i18n opt-out, so if you don't intend to use it, set `no_i18n`
105-
/// to `true` in `locales`. Note that you must still specify a default locale for verbosity and correctness. If you specify `no_i18n` and
106-
/// a custom translations manager, the latter will override.
139+
/// compatibility with the Perseus CLI significantly easier.
107140
///
108-
/// Warning: all properties must currently be in the correct order (`root`, `templates`, `error_pages`, `locales`, `config_manager`,
109-
/// `translations_manager`).
110-
// TODO make this syntax even more compact and beautiful? (error pages inside templates?)
141+
/// Warning: all properties must currently be in the correct order (`root`, `templates`, `error_pages`, `locales`, `static_aliases`,
142+
/// `config_manager`, `translations_manager`).
111143
#[macro_export]
112144
macro_rules! define_app {
113145
// With locales
@@ -124,6 +156,9 @@ macro_rules! define_app {
124156
other: [$($other_locale:literal),*]
125157
$(,no_i18n: $no_i18n:literal)?
126158
}
159+
$(,static_aliases: {
160+
$($url:literal => $resource:literal)*
161+
})?
127162
$(,config_manager: $config_manager:expr)?
128163
$(,translations_manager: $translations_manager:expr)?
129164
} => {
@@ -140,6 +175,9 @@ macro_rules! define_app {
140175
// The user doesn't have to define any other locales (but they'll still get locale detection and the like)
141176
other: [$($other_locale),*]
142177
}
178+
$(,static_aliases: {
179+
$($url => $resource)*
180+
})?
143181
$(,config_manager: $config_manager)?
144182
$(,translations_manager: $translations_manager)?
145183
}
@@ -152,6 +190,9 @@ macro_rules! define_app {
152190
$($template:expr),+
153191
],
154192
error_pages: $error_pages:expr
193+
$(,static_aliases: {
194+
$($url:literal => $resource:literal)*
195+
})?
155196
$(,config_manager: $config_manager:expr)?
156197
$(,translations_manager: $translations_manager:expr)?
157198
} => {
@@ -169,6 +210,9 @@ macro_rules! define_app {
169210
other: [],
170211
no_i18n: true
171212
}
213+
$(,static_aliases: {
214+
$($url => $resource)*
215+
})?
172216
$(,config_manager: $config_manager)?
173217
$(,translations_manager: $translations_manager)?
174218
}
@@ -191,6 +235,9 @@ macro_rules! define_app {
191235
// If this is defined at all, i18n will be disabled and the default locale will be set to `xx-XX`
192236
$(,no_i18n: $no_i18n:literal)?
193237
}
238+
$(,static_aliases: {
239+
$($url:literal => $resource:literal)*
240+
})?
194241
$(,config_manager: $config_manager:expr)?
195242
$(,translations_manager: $translations_manager:expr)?
196243
}
@@ -235,5 +282,12 @@ macro_rules! define_app {
235282
pub fn get_error_pages<G: $crate::GenericNode>() -> $crate::ErrorPages<G> {
236283
$error_pages
237284
}
285+
286+
/// Gets any static content aliases provided by the user.
287+
$crate::define_get_static_aliases!(
288+
$(static_aliases: {
289+
$($url => $resource)*
290+
})?
291+
);
238292
};
239293
}

0 commit comments

Comments
 (0)