From 4dc0dd165f399dba8ac3620c76806a8c8fd8e642 Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Sat, 6 Nov 2021 20:04:40 +1100 Subject: [PATCH 1/3] =?UTF-8?q?fix(cli):=20=F0=9F=90=9B=20switched=20to=20?= =?UTF-8?q?`create=5Fdir=5Fall`=20in=20`delete=5Fartifacts`=20in=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This *should* fix #69, but we'll see. --- packages/perseus-cli/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/perseus-cli/src/lib.rs b/packages/perseus-cli/src/lib.rs index 25190141b2..3c0a5f458b 100644 --- a/packages/perseus-cli/src/lib.rs +++ b/packages/perseus-cli/src/lib.rs @@ -84,7 +84,8 @@ pub fn delete_artifacts(dir: PathBuf, dir_to_remove: &str) -> Result<(), Executi } } // No matter what, it's gone now, so recreate it - if let Err(err) = fs::create_dir(&target) { + // We also create parent directories because that's an issue for some reason in Docker (see #69) + if let Err(err) = fs::create_dir_all(&target) { return Err(ExecutionError::RemoveArtifactsFailed { target: target.to_str().map(|s| s.to_string()), source: err, From 5660658e2a7fb741841200eb754ca062883b35e1 Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Sun, 7 Nov 2021 12:04:30 +1100 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20=F0=9F=99=88=20added=20copied=20`.?= =?UTF-8?q?perseus/`=20folder=20in=20cli=20to=20git?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should make compiling for obscure Docker environments *far* easier while fixing! --- packages/perseus-cli/.gitignore | 3 +- packages/perseus-cli/.perseus/.gitignore | 2 + packages/perseus-cli/.perseus/Cargo.toml.old | 25 +++ .../.perseus/builder/Cargo.toml.old | 30 ++++ .../.perseus/builder/src/bin/build.rs | 59 +++++++ .../.perseus/builder/src/bin/export.rs | 147 ++++++++++++++++++ .../.perseus/builder/src/bin/tinker.rs | 22 +++ .../.perseus/server/Cargo.toml.old | 16 ++ .../perseus-cli/.perseus/server/src/main.rs | 99 ++++++++++++ packages/perseus-cli/.perseus/src/app.rs | 144 +++++++++++++++++ packages/perseus-cli/.perseus/src/lib.rs | 145 +++++++++++++++++ 11 files changed, 691 insertions(+), 1 deletion(-) create mode 100644 packages/perseus-cli/.perseus/.gitignore create mode 100644 packages/perseus-cli/.perseus/Cargo.toml.old create mode 100644 packages/perseus-cli/.perseus/builder/Cargo.toml.old create mode 100644 packages/perseus-cli/.perseus/builder/src/bin/build.rs create mode 100644 packages/perseus-cli/.perseus/builder/src/bin/export.rs create mode 100644 packages/perseus-cli/.perseus/builder/src/bin/tinker.rs create mode 100644 packages/perseus-cli/.perseus/server/Cargo.toml.old create mode 100644 packages/perseus-cli/.perseus/server/src/main.rs create mode 100644 packages/perseus-cli/.perseus/src/app.rs create mode 100644 packages/perseus-cli/.perseus/src/lib.rs diff --git a/packages/perseus-cli/.gitignore b/packages/perseus-cli/.gitignore index 5da7517caf..639d77c366 100644 --- a/packages/perseus-cli/.gitignore +++ b/packages/perseus-cli/.gitignore @@ -1,2 +1,3 @@ # We copy in the subcrate under development in the `cli` example for packaging only -.perseus/ +# TODO change this back after fixing #69 is done! +!.perseus/ diff --git a/packages/perseus-cli/.perseus/.gitignore b/packages/perseus-cli/.perseus/.gitignore new file mode 100644 index 0000000000..5076b767e0 --- /dev/null +++ b/packages/perseus-cli/.perseus/.gitignore @@ -0,0 +1,2 @@ +dist/ +target/ diff --git a/packages/perseus-cli/.perseus/Cargo.toml.old b/packages/perseus-cli/.perseus/Cargo.toml.old new file mode 100644 index 0000000000..9cd087c32b --- /dev/null +++ b/packages/perseus-cli/.perseus/Cargo.toml.old @@ -0,0 +1,25 @@ +# This crate defines the user's app in terms that Wasm can understand, making development significantly simpler. +# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it! + +[package] +name = "perseus-engine" +version = "0.3.0-beta.16" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# We alias here because the package name will change based on whatever's in the user's manifest +app = { package = "perseus-example-basic", path = "../" } + +perseus = { path = "../../../packages/perseus" } +sycamore = { version = "0.6", features = ["ssr"] } +sycamore-router = "0.6" +web-sys = { version = "0.3", features = ["Event", "Headers", "Request", "RequestInit", "RequestMode", "Response", "ReadableStream", "Window"] } +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +console_error_panic_hook = "0.1.6" + +# This section is needed for Wasm Pack (which we use instead of Trunk for flexibility) +[lib] +crate-type = ["cdylib", "rlib"] diff --git a/packages/perseus-cli/.perseus/builder/Cargo.toml.old b/packages/perseus-cli/.perseus/builder/Cargo.toml.old new file mode 100644 index 0000000000..78fea7ef92 --- /dev/null +++ b/packages/perseus-cli/.perseus/builder/Cargo.toml.old @@ -0,0 +1,30 @@ +# This crate defines the build process for Perseus +# This used to be part of the root crate in the engine, but it was moved out of there so feature gating could be different across the server, builder, and client +# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it! + +[package] +name = "perseus-engine-builder" +version = "0.3.0-beta.16" +edition = "2018" +default-run = "perseus-builder" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +perseus-engine = { path = "../" } +perseus = { path = "../../../../packages/perseus", features = [ "tinker-plugins", "server-side" ] } +futures = "0.3" +fs_extra = "1" + +# We define a binary for building, serving, and doing both +[[bin]] +name = "perseus-builder" +path = "src/bin/build.rs" + +[[bin]] +name = "perseus-exporter" +path = "src/bin/export.rs" + +[[bin]] +name = "perseus-tinker" # Yes, the noun is 'tinker', not 'tinkerer' +path = "src/bin/tinker.rs" diff --git a/packages/perseus-cli/.perseus/builder/src/bin/build.rs b/packages/perseus-cli/.perseus/builder/src/bin/build.rs new file mode 100644 index 0000000000..a0713cbd0d --- /dev/null +++ b/packages/perseus-cli/.perseus/builder/src/bin/build.rs @@ -0,0 +1,59 @@ +use futures::executor::block_on; +use perseus::{internal::build::build_app, PluginAction, SsrNode}; +use perseus_engine::app::{ + get_immutable_store, get_locales, get_mutable_store, get_plugins, get_templates_map, + get_translations_manager, +}; + +fn main() { + let exit_code = real_main(); + std::process::exit(exit_code) +} + +fn real_main() -> i32 { + // We want to be working in the root of `.perseus/` + std::env::set_current_dir("../").unwrap(); + let plugins = get_plugins::(); + + plugins + .functional_actions + .build_actions + .before_build + .run((), plugins.get_plugin_data()); + + let immutable_store = get_immutable_store(&plugins); + let mutable_store = get_mutable_store(); + let translations_manager = block_on(get_translations_manager()); + let locales = get_locales(&plugins); + + // Build the site for all the common locales (done in parallel) + // All these parameters can be modified by `define_app!` and plugins, so there's no point in having a plugin opportunity here + let templates_map = get_templates_map::(&plugins); + let fut = build_app( + &templates_map, + &locales, + (&immutable_store, &mutable_store), + &translations_manager, + // We use another binary to handle exporting + false, + ); + let res = block_on(fut); + if let Err(err) = res { + let err_msg = format!("Static generation failed: '{}'.", &err); + plugins + .functional_actions + .build_actions + .after_failed_build + .run(err, plugins.get_plugin_data()); + eprintln!("{}", err_msg); + 1 + } else { + plugins + .functional_actions + .build_actions + .after_successful_build + .run((), plugins.get_plugin_data()); + println!("Static generation successfully completed!"); + 0 + } +} diff --git a/packages/perseus-cli/.perseus/builder/src/bin/export.rs b/packages/perseus-cli/.perseus/builder/src/bin/export.rs new file mode 100644 index 0000000000..fb6c4b9f37 --- /dev/null +++ b/packages/perseus-cli/.perseus/builder/src/bin/export.rs @@ -0,0 +1,147 @@ +use fs_extra::dir::{copy as copy_dir, CopyOptions}; +use futures::executor::block_on; +use perseus::{ + internal::{build::build_app, export::export_app, get_path_prefix_server}, + PluginAction, SsrNode, +}; +use perseus_engine::app::{ + get_app_root, get_immutable_store, get_locales, get_mutable_store, get_plugins, + get_static_aliases, get_templates_map, get_translations_manager, +}; +use std::fs; +use std::path::PathBuf; + +fn main() { + let exit_code = real_main(); + std::process::exit(exit_code) +} + +fn real_main() -> i32 { + // We want to be working in the root of `.perseus/` + std::env::set_current_dir("../").unwrap(); + + let plugins = get_plugins::(); + + plugins + .functional_actions + .build_actions + .before_build + .run((), plugins.get_plugin_data()); + + let immutable_store = get_immutable_store(&plugins); + // We don't need this in exporting, but the build process does + let mutable_store = get_mutable_store(); + let translations_manager = block_on(get_translations_manager()); + let locales = get_locales(&plugins); + + // Build the site for all the common locales (done in parallel), denying any non-exportable features + let templates_map = get_templates_map::(&plugins); + let build_fut = build_app( + &templates_map, + &locales, + (&immutable_store, &mutable_store), + &translations_manager, + // We use another binary to handle normal building + true, + ); + if let Err(err) = block_on(build_fut) { + let err_msg = format!("Static exporting failed: '{}'.", &err); + plugins + .functional_actions + .export_actions + .after_failed_build + .run(err, plugins.get_plugin_data()); + eprintln!("{}", err_msg); + return 1; + } + plugins + .functional_actions + .export_actions + .after_successful_build + .run((), plugins.get_plugin_data()); + // Turn the build artifacts into self-contained static files + let app_root = get_app_root(&plugins); + let export_fut = export_app( + &templates_map, + // Perseus always uses one HTML file, and there's no point in letting a plugin change that + "../index.html", + &locales, + &app_root, + &immutable_store, + &translations_manager, + get_path_prefix_server(), + ); + if let Err(err) = block_on(export_fut) { + let err_msg = format!("Static exporting failed: '{}'.", &err); + plugins + .functional_actions + .export_actions + .after_failed_export + .run(err, plugins.get_plugin_data()); + eprintln!("{}", err_msg); + return 1; + } + + // Copy the `static` directory into the export package if it exists + // If the user wants extra, they can use static aliases, plugins are unnecessary here + let static_dir = PathBuf::from("../static"); + if static_dir.exists() { + if let Err(err) = copy_dir(&static_dir, "dist/exported/.perseus/", &CopyOptions::new()) { + let err_msg = format!( + "Static exporting failed: 'couldn't copy static directory: '{}''", + &err + ); + plugins + .functional_actions + .export_actions + .after_failed_static_copy + .run(err.to_string(), plugins.get_plugin_data()); + eprintln!("{}", err_msg); + return 1; + } + } + // Loop through any static aliases and copy them in too + // Unlike with the server, these could override pages! + // We'll copy from the alias to the path (it could be a directory or a file) + // Remember: `alias` has a leading `/`! + for (alias, path) in get_static_aliases(&plugins) { + let from = PathBuf::from(path); + let to = format!("dist/exported{}", alias); + + if from.is_dir() { + if let Err(err) = copy_dir(&from, &to, &CopyOptions::new()) { + let err_msg = format!( + "Static exporting failed: 'couldn't copy static alias directory: '{}''", + err.to_string() + ); + plugins + .functional_actions + .export_actions + .after_failed_static_alias_dir_copy + .run(err.to_string(), plugins.get_plugin_data()); + eprintln!("{}", err_msg); + return 1; + } + } else if let Err(err) = fs::copy(&from, &to) { + let err_msg = format!( + "Static exporting failed: 'couldn't copy static alias file: '{}''", + err.to_string() + ); + plugins + .functional_actions + .export_actions + .after_failed_static_alias_file_copy + .run(err, plugins.get_plugin_data()); + eprintln!("{}", err_msg); + return 1; + } + } + + plugins + .functional_actions + .export_actions + .after_successful_export + .run((), plugins.get_plugin_data()); + println!("Static exporting successfully completed!"); + 0 +} diff --git a/packages/perseus-cli/.perseus/builder/src/bin/tinker.rs b/packages/perseus-cli/.perseus/builder/src/bin/tinker.rs new file mode 100644 index 0000000000..f104ffa569 --- /dev/null +++ b/packages/perseus-cli/.perseus/builder/src/bin/tinker.rs @@ -0,0 +1,22 @@ +use perseus::{plugins::PluginAction, SsrNode}; +use perseus_engine::app::get_plugins; + +fn main() { + let exit_code = real_main(); + std::process::exit(exit_code) +} + +fn real_main() -> i32 { + // We want to be working in the root of `.perseus/` + std::env::set_current_dir("../").unwrap(); + + let plugins = get_plugins::(); + // Run all the tinker actions + plugins + .functional_actions + .tinker + .run((), plugins.get_plugin_data()); + + println!("Tinkering complete!"); + 0 +} diff --git a/packages/perseus-cli/.perseus/server/Cargo.toml.old b/packages/perseus-cli/.perseus/server/Cargo.toml.old new file mode 100644 index 0000000000..0253868582 --- /dev/null +++ b/packages/perseus-cli/.perseus/server/Cargo.toml.old @@ -0,0 +1,16 @@ +# This crate defines the user's app in terms that Wasm can understand, making development significantly simpler. +# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it! + +[package] +name = "perseus-engine-server" +version = "0.3.0-beta.16" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +perseus = { path = "../../../../packages/perseus", features = [ "server-side" ] } +perseus-actix-web = { path = "../../../../packages/perseus-actix-web" } +perseus-engine = { path = "../" } +actix-web = "3.3" +futures = "0.3" diff --git a/packages/perseus-cli/.perseus/server/src/main.rs b/packages/perseus-cli/.perseus/server/src/main.rs new file mode 100644 index 0000000000..73ee3ca290 --- /dev/null +++ b/packages/perseus-cli/.perseus/server/src/main.rs @@ -0,0 +1,99 @@ +use actix_web::{App, HttpServer}; +use futures::executor::block_on; +use perseus::plugins::PluginAction; +use perseus::SsrNode; +use perseus_actix_web::{configurer, Options}; +use perseus_engine::app::{ + get_app_root, get_error_pages_contained, get_immutable_store, get_locales, get_mutable_store, + get_plugins, get_static_aliases, get_templates_map_contained, get_translations_manager, +}; +use std::collections::HashMap; +use std::env; +use std::fs; + +// This server executable can be run in two modes: +// dev: inside `.perseus/server/src/main.rs`, works with that file structure +// prod: as a standalone executable with a `dist/` directory as a sibling +// The prod mode can be enabled by setting the `PERSEUS_STANDALONE` environment variable + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let plugins = get_plugins::(); + + // So we don't have to define a different `FsConfigManager` just for the server, we shift the execution context to the same level as everything else + // The server has to be a separate crate because otherwise the dependencies don't work with Wasm bundling + // If we're not running as a standalone binary, assume we're running in dev mode under `.perseus/` + let is_standalone; + if env::var("PERSEUS_STANDALONE").is_err() { + env::set_current_dir("../").unwrap(); + is_standalone = false; + } else { + // If we are running as a standalone binary, we have no idea where we're being executed from (#63), so we should set the working directory to be the same as the binary location + let binary_loc = env::current_exe().unwrap(); + let binary_dir = binary_loc.parent().unwrap(); // It's a file, there's going to be a parent if we're working on anything close to sanity + env::set_current_dir(binary_dir).unwrap(); + is_standalone = true; + } + + plugins + .functional_actions + .server_actions + .before_serve + .run((), plugins.get_plugin_data()); + + // This allows us to operate inside `.perseus/` and as a standalone binary in production + let (html_shell_path, static_dir_path) = if is_standalone { + ("./index.html", "./static") + } else { + ("../index.html", "../static") + }; + + let host = env::var("HOST").unwrap_or_else(|_| "localhost".to_string()); + let port = env::var("PORT") + .unwrap_or_else(|_| "8080".to_string()) + .parse::(); + if let Ok(port) = port { + let immutable_store = get_immutable_store(&plugins); + let locales = get_locales(&plugins); + let app_root = get_app_root(&plugins); + let static_aliases = get_static_aliases(&plugins); + HttpServer::new(move || { + // TODO find a way to configure the server with plugins without using `actix-web` in the `perseus` crate (it won't compile to Wasm) + App::new().configure(block_on(configurer( + Options { + // We don't support setting some attributes from `wasm-pack` through plugins/`define_app!` because that would require CLI changes as well (a job for an alternative engine) + index: html_shell_path.to_string(), // The user must define their own `index.html` file + js_bundle: "dist/pkg/perseus_engine.js".to_string(), + // Our crate has the same name, so this will be predictable + wasm_bundle: "dist/pkg/perseus_engine_bg.wasm".to_string(), + // It's a nightmare to get the templates map to take plugins, so we use a self-contained version + // TODO reduce allocations here + templates_map: get_templates_map_contained(), + locales: locales.clone(), + root_id: app_root.to_string(), + snippets: "dist/pkg/snippets".to_string(), + error_pages: get_error_pages_contained(), + // 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_dir_path).is_ok() { + let mut static_dirs = HashMap::new(); + static_dirs.insert("".to_string(), static_dir_path.to_string()); + static_dirs + } else { + HashMap::new() + }, + static_aliases: static_aliases.clone(), + }, + immutable_store.clone(), + get_mutable_store(), + block_on(get_translations_manager()), + ))) + }) + .bind((host, port))? + .run() + .await + } else { + eprintln!("Port must be a number."); + Ok(()) + } +} diff --git a/packages/perseus-cli/.perseus/src/app.rs b/packages/perseus-cli/.perseus/src/app.rs new file mode 100644 index 0000000000..65780d24ff --- /dev/null +++ b/packages/perseus-cli/.perseus/src/app.rs @@ -0,0 +1,144 @@ +// This file is used for processing data from the `define_app!` macro +// It also applies plugin opportunities for changing aspects thereof + +pub use app::get_plugins; +use perseus::{ + internal::i18n::Locales, stores::ImmutableStore, templates::TemplateMap, ErrorPages, + GenericNode, PluginAction, Plugins, +}; +use std::collections::HashMap; + +pub use app::{get_mutable_store, get_translations_manager}; + +// These functions all take plugins so we don't have to perform possibly very costly allocation more than once in an environment (e.g. browser, build process, export process, server) + +// pub fn get_mutable_store() -> impl MutableStore { +// todo!() +// } +pub fn get_immutable_store(plugins: &Plugins) -> ImmutableStore { + let immutable_store = app::get_immutable_store(); + plugins + .control_actions + .settings_actions + .set_immutable_store + .run(immutable_store.clone(), plugins.get_plugin_data()) + .unwrap_or(immutable_store) +} +pub fn get_app_root(plugins: &Plugins) -> String { + plugins + .control_actions + .settings_actions + .set_app_root + .run((), plugins.get_plugin_data()) + .unwrap_or_else(|| app::APP_ROOT.to_string()) +} +// pub async fn get_translations_manager() -> impl TranslationsManager { +// todo!() +// } +pub fn get_locales(plugins: &Plugins) -> Locales { + let locales = app::get_locales(); + plugins + .control_actions + .settings_actions + .set_locales + .run(locales.clone(), plugins.get_plugin_data()) + .unwrap_or(locales) +} +// This also performs rescoping and security checks so that we don't include anything outside the project root +pub fn get_static_aliases(plugins: &Plugins) -> HashMap { + let mut static_aliases = app::get_static_aliases(); + // This will return a map of plugin name to another map of static aliases that that plugin produced + let extra_static_aliases = plugins + .functional_actions + .settings_actions + .add_static_aliases + .run((), plugins.get_plugin_data()); + for (_plugin_name, aliases) in extra_static_aliases { + let new_aliases: HashMap = aliases + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + static_aliases.extend(new_aliases); + } + + let mut scoped_static_aliases = HashMap::new(); + for (url, path) in static_aliases { + // 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 new_path = if path.starts_with('/') { + // Absolute paths are a security risk and are disallowed + panic!( + "it's a security risk to include absolute paths in `static_aliases` ('{}')", + path + ); + } else if path.starts_with("../") { + // Anything outside this directory is a security risk as well + panic!("it's a security risk to include paths outside the current directory in `static_aliases` ('{}')", path); + } else if path.starts_with("./") { + // `./` -> `../` (moving to execution from `.perseus/`) + // But if we're operating standalone, it stays the same + if ::std::env::var("PERSEUS_STANDALONE").is_ok() { + path.to_string() + } else { + format!(".{}", path) + } + } else { + // Anything else gets a `../` prepended + // But if we're operating standalone, it stays the same + if ::std::env::var("PERSEUS_STANDALONE").is_ok() { + path.to_string() + } else { + format!("../{}", path) + } + }; + + scoped_static_aliases.insert(url, new_path); + } + + scoped_static_aliases +} +// This doesn't take plugins because that would actually increase allocation and indirection on the server +pub fn get_templates_map(plugins: &Plugins) -> TemplateMap { + let mut templates = app::get_templates_map::(); + // This will return a map of plugin name to a vector of templates to add + let extra_templates = plugins + .functional_actions + .settings_actions + .add_templates + .run((), plugins.get_plugin_data()); + for (_plugin_name, plugin_templates) in extra_templates { + // Turn that vector into a template map by extracting the template root paths as keys + for template in plugin_templates { + templates.insert(template.get_path(), template); + } + } + + templates +} +pub fn get_error_pages(plugins: &Plugins) -> ErrorPages { + let mut error_pages = app::get_error_pages::(); + // This will return a map of plugin name to a map of status codes to error pages + let extra_error_pages = plugins + .functional_actions + .settings_actions + .add_error_pages + .run((), plugins.get_plugin_data()); + for (_plugin_name, plugin_error_pages) in extra_error_pages { + for (status, error_page) in plugin_error_pages { + error_pages.add_page_rc(status, error_page); + } + } + + error_pages +} + +// We provide alternatives for `get_templates_map` and `get_error_pages` that get their own plugins +// This avoids major allocation/sync problems on the server +pub fn get_templates_map_contained() -> TemplateMap { + let plugins = get_plugins::(); + get_templates_map(&plugins) +} +pub fn get_error_pages_contained() -> ErrorPages { + let plugins = get_plugins::(); + get_error_pages(&plugins) +} diff --git a/packages/perseus-cli/.perseus/src/lib.rs b/packages/perseus-cli/.perseus/src/lib.rs new file mode 100644 index 0000000000..2c3646ed9b --- /dev/null +++ b/packages/perseus-cli/.perseus/src/lib.rs @@ -0,0 +1,145 @@ +pub mod app; + +use crate::app::{get_app_root, get_error_pages, get_locales, get_plugins, get_templates_map}; +use perseus::{ + checkpoint, create_app_route, + internal::{ + error_pages::ErrorPageData, + i18n::{detect_locale, ClientTranslationsManager}, + router::{RouteInfo, RouteVerdict}, + shell::{app_shell, get_initial_state, get_render_cfg, InitialState}, + }, + plugins::PluginAction, + DomNode, +}; +use std::cell::RefCell; +use std::rc::Rc; +use sycamore::prelude::{cloned, create_effect, template, NodeRef, StateHandle}; +use sycamore_router::{HistoryIntegration, Router, RouterProps}; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + +/// The entrypoint into the app itself. This will be compiled to Wasm and actually executed, rendering the rest of the app. +#[wasm_bindgen] +pub fn run() -> Result<(), JsValue> { + let plugins = get_plugins::(); + + checkpoint("begin"); + // Panics should always go to the console + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + + plugins + .functional_actions + .client_actions + .start + .run((), plugins.get_plugin_data()); + checkpoint("initial_plugins_complete"); + + // Get the root we'll be injecting the router into + let root = web_sys::window() + .unwrap() + .document() + .unwrap() + .query_selector(&format!("#{}", get_app_root(&plugins))) + .unwrap() + .unwrap(); + + // Get the root that the server will have injected initial load content into + // This will be moved into a reactive `
` by the app shell + // This is an `Option` until we know we aren't doing locale detection (in which case it wouldn't exist) + let initial_container = web_sys::window() + .unwrap() + .document() + .unwrap() + .query_selector("#__perseus_content_initial") + .unwrap(); + // And create a node reference that we can use as a handle to the reactive verison + let container_rx = NodeRef::new(); + + // Create a mutable translations manager to control caching + let locales = get_locales(&plugins); + let translations_manager = Rc::new(RefCell::new(ClientTranslationsManager::new(&locales))); + // Get the error pages in an `Rc` so we aren't creating hundreds of them + let error_pages = Rc::new(get_error_pages(&plugins)); + + // Create the router we'll use for this app, based on the user's app definition + create_app_route! { + name => AppRoute, + // The render configuration is injected verbatim into the HTML shell, so it certainly should be present + render_cfg => &get_render_cfg().expect("render configuration invalid or not injected"), + // TODO avoid unnecessary allocation here (major problem!) + // The `G` parameter is ambient here for `RouteVerdict` + templates => &get_templates_map::(&get_plugins()), + locales => &get_locales::(&get_plugins()) + } + + // Put the locales into an `Rc` so we can use them in locale detection (which is inside a future) + let locales = Rc::new(locales); + + sycamore::render_to( + move || { + template! { + Router(RouterProps::new(HistoryIntegration::new(), move |route: StateHandle>| { + create_effect(cloned!((container_rx) => move || { + // Sycamore's reactivity is broken by a future, so we need to explicitly add the route to the reactive dependencies here + // We do need the future though (otherwise `container_rx` doesn't link to anything until it's too late) + let _ = route.get(); + wasm_bindgen_futures::spawn_local(cloned!((locales, route, container_rx, translations_manager, error_pages, initial_container) => async move { + let container_rx_elem = container_rx.get::().unchecked_into::(); + checkpoint("router_entry"); + match &route.get().as_ref().0 { + // Perseus' custom routing system is tightly coupled to the template system, and returns exactly what we need for the app shell! + // If a non-404 error occurred, it will be handled in the app shell + RouteVerdict::Found(RouteInfo { + path, + template, + locale, + was_incremental_match + }) => app_shell( + path.clone(), + (template.clone(), *was_incremental_match), + locale.clone(), + // We give the app shell a translations manager and let it get the `Rc` itself (because it can do async safely) + Rc::clone(&translations_manager), + Rc::clone(&error_pages), + (initial_container.unwrap().clone(), container_rx_elem.clone()) + ).await, + // If the user is using i18n, then they'll want to detect the locale on any paths missing a locale + // Those all go to the same system that redirects to the appropriate locale + // Note that `container` doesn't exist for this scenario + RouteVerdict::LocaleDetection(path) => detect_locale(path.clone(), &locales), + // To get a translator here, we'd have to go async and dangerously check the URL + // If this is an initial load, there'll already be an error message, so we should only proceed if the declaration is not `error` + RouteVerdict::NotFound => { + checkpoint("not_found"); + if let InitialState::Error(ErrorPageData { url, status, err }) = get_initial_state() { + let initial_container = initial_container.unwrap(); + // We need to move the server-rendered content from its current container to the reactive container (otherwise Sycamore can't work with it properly) + let initial_html = initial_container.inner_html(); + container_rx_elem.set_inner_html(&initial_html); + initial_container.set_inner_html(""); + // Make the initial container invisible + initial_container.set_attribute("style", "display: none;").unwrap(); + // Hydrate the error pages + // Right now, we don't provide translators to any error pages that have come from the server + error_pages.hydrate_page(&url, &status, &err, None, &container_rx_elem); + } else { + error_pages.hydrate_page("", &404, "not found", None, &container_rx_elem); + } + }, + }; + })); + })); + // This template is reactive, and will be updated as necessary + // However, the server has already rendered initial load content elsewhere, so we move that into here as well in the app shell + // The main reason for this is that the router only intercepts click events from its children + template! { + div(id="__perseus_content_rx", class="__perseus_content", ref=container_rx) {} + } + })) + } + }, + &root, + ); + + Ok(()) +} From 077c62ab6cf9d23d770a6ac19b23f18b7e5aacc3 Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Sun, 7 Nov 2021 13:58:58 +1100 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=F0=9F=99=88=20removed=20build=20a?= =?UTF-8?q?rtifacts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These were only for testing in Docker, and they're no longer required. --- packages/perseus-cli/.gitignore | 3 +- packages/perseus-cli/.perseus/.gitignore | 2 - packages/perseus-cli/.perseus/Cargo.toml.old | 25 --- .../.perseus/builder/Cargo.toml.old | 30 ---- .../.perseus/builder/src/bin/build.rs | 59 ------- .../.perseus/builder/src/bin/export.rs | 147 ------------------ .../.perseus/builder/src/bin/tinker.rs | 22 --- .../.perseus/server/Cargo.toml.old | 16 -- .../perseus-cli/.perseus/server/src/main.rs | 99 ------------ packages/perseus-cli/.perseus/src/app.rs | 144 ----------------- packages/perseus-cli/.perseus/src/lib.rs | 145 ----------------- 11 files changed, 1 insertion(+), 691 deletions(-) delete mode 100644 packages/perseus-cli/.perseus/.gitignore delete mode 100644 packages/perseus-cli/.perseus/Cargo.toml.old delete mode 100644 packages/perseus-cli/.perseus/builder/Cargo.toml.old delete mode 100644 packages/perseus-cli/.perseus/builder/src/bin/build.rs delete mode 100644 packages/perseus-cli/.perseus/builder/src/bin/export.rs delete mode 100644 packages/perseus-cli/.perseus/builder/src/bin/tinker.rs delete mode 100644 packages/perseus-cli/.perseus/server/Cargo.toml.old delete mode 100644 packages/perseus-cli/.perseus/server/src/main.rs delete mode 100644 packages/perseus-cli/.perseus/src/app.rs delete mode 100644 packages/perseus-cli/.perseus/src/lib.rs diff --git a/packages/perseus-cli/.gitignore b/packages/perseus-cli/.gitignore index 639d77c366..5da7517caf 100644 --- a/packages/perseus-cli/.gitignore +++ b/packages/perseus-cli/.gitignore @@ -1,3 +1,2 @@ # We copy in the subcrate under development in the `cli` example for packaging only -# TODO change this back after fixing #69 is done! -!.perseus/ +.perseus/ diff --git a/packages/perseus-cli/.perseus/.gitignore b/packages/perseus-cli/.perseus/.gitignore deleted file mode 100644 index 5076b767e0..0000000000 --- a/packages/perseus-cli/.perseus/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist/ -target/ diff --git a/packages/perseus-cli/.perseus/Cargo.toml.old b/packages/perseus-cli/.perseus/Cargo.toml.old deleted file mode 100644 index 9cd087c32b..0000000000 --- a/packages/perseus-cli/.perseus/Cargo.toml.old +++ /dev/null @@ -1,25 +0,0 @@ -# This crate defines the user's app in terms that Wasm can understand, making development significantly simpler. -# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it! - -[package] -name = "perseus-engine" -version = "0.3.0-beta.16" -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# We alias here because the package name will change based on whatever's in the user's manifest -app = { package = "perseus-example-basic", path = "../" } - -perseus = { path = "../../../packages/perseus" } -sycamore = { version = "0.6", features = ["ssr"] } -sycamore-router = "0.6" -web-sys = { version = "0.3", features = ["Event", "Headers", "Request", "RequestInit", "RequestMode", "Response", "ReadableStream", "Window"] } -wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" -console_error_panic_hook = "0.1.6" - -# This section is needed for Wasm Pack (which we use instead of Trunk for flexibility) -[lib] -crate-type = ["cdylib", "rlib"] diff --git a/packages/perseus-cli/.perseus/builder/Cargo.toml.old b/packages/perseus-cli/.perseus/builder/Cargo.toml.old deleted file mode 100644 index 78fea7ef92..0000000000 --- a/packages/perseus-cli/.perseus/builder/Cargo.toml.old +++ /dev/null @@ -1,30 +0,0 @@ -# This crate defines the build process for Perseus -# This used to be part of the root crate in the engine, but it was moved out of there so feature gating could be different across the server, builder, and client -# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it! - -[package] -name = "perseus-engine-builder" -version = "0.3.0-beta.16" -edition = "2018" -default-run = "perseus-builder" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -perseus-engine = { path = "../" } -perseus = { path = "../../../../packages/perseus", features = [ "tinker-plugins", "server-side" ] } -futures = "0.3" -fs_extra = "1" - -# We define a binary for building, serving, and doing both -[[bin]] -name = "perseus-builder" -path = "src/bin/build.rs" - -[[bin]] -name = "perseus-exporter" -path = "src/bin/export.rs" - -[[bin]] -name = "perseus-tinker" # Yes, the noun is 'tinker', not 'tinkerer' -path = "src/bin/tinker.rs" diff --git a/packages/perseus-cli/.perseus/builder/src/bin/build.rs b/packages/perseus-cli/.perseus/builder/src/bin/build.rs deleted file mode 100644 index a0713cbd0d..0000000000 --- a/packages/perseus-cli/.perseus/builder/src/bin/build.rs +++ /dev/null @@ -1,59 +0,0 @@ -use futures::executor::block_on; -use perseus::{internal::build::build_app, PluginAction, SsrNode}; -use perseus_engine::app::{ - get_immutable_store, get_locales, get_mutable_store, get_plugins, get_templates_map, - get_translations_manager, -}; - -fn main() { - let exit_code = real_main(); - std::process::exit(exit_code) -} - -fn real_main() -> i32 { - // We want to be working in the root of `.perseus/` - std::env::set_current_dir("../").unwrap(); - let plugins = get_plugins::(); - - plugins - .functional_actions - .build_actions - .before_build - .run((), plugins.get_plugin_data()); - - let immutable_store = get_immutable_store(&plugins); - let mutable_store = get_mutable_store(); - let translations_manager = block_on(get_translations_manager()); - let locales = get_locales(&plugins); - - // Build the site for all the common locales (done in parallel) - // All these parameters can be modified by `define_app!` and plugins, so there's no point in having a plugin opportunity here - let templates_map = get_templates_map::(&plugins); - let fut = build_app( - &templates_map, - &locales, - (&immutable_store, &mutable_store), - &translations_manager, - // We use another binary to handle exporting - false, - ); - let res = block_on(fut); - if let Err(err) = res { - let err_msg = format!("Static generation failed: '{}'.", &err); - plugins - .functional_actions - .build_actions - .after_failed_build - .run(err, plugins.get_plugin_data()); - eprintln!("{}", err_msg); - 1 - } else { - plugins - .functional_actions - .build_actions - .after_successful_build - .run((), plugins.get_plugin_data()); - println!("Static generation successfully completed!"); - 0 - } -} diff --git a/packages/perseus-cli/.perseus/builder/src/bin/export.rs b/packages/perseus-cli/.perseus/builder/src/bin/export.rs deleted file mode 100644 index fb6c4b9f37..0000000000 --- a/packages/perseus-cli/.perseus/builder/src/bin/export.rs +++ /dev/null @@ -1,147 +0,0 @@ -use fs_extra::dir::{copy as copy_dir, CopyOptions}; -use futures::executor::block_on; -use perseus::{ - internal::{build::build_app, export::export_app, get_path_prefix_server}, - PluginAction, SsrNode, -}; -use perseus_engine::app::{ - get_app_root, get_immutable_store, get_locales, get_mutable_store, get_plugins, - get_static_aliases, get_templates_map, get_translations_manager, -}; -use std::fs; -use std::path::PathBuf; - -fn main() { - let exit_code = real_main(); - std::process::exit(exit_code) -} - -fn real_main() -> i32 { - // We want to be working in the root of `.perseus/` - std::env::set_current_dir("../").unwrap(); - - let plugins = get_plugins::(); - - plugins - .functional_actions - .build_actions - .before_build - .run((), plugins.get_plugin_data()); - - let immutable_store = get_immutable_store(&plugins); - // We don't need this in exporting, but the build process does - let mutable_store = get_mutable_store(); - let translations_manager = block_on(get_translations_manager()); - let locales = get_locales(&plugins); - - // Build the site for all the common locales (done in parallel), denying any non-exportable features - let templates_map = get_templates_map::(&plugins); - let build_fut = build_app( - &templates_map, - &locales, - (&immutable_store, &mutable_store), - &translations_manager, - // We use another binary to handle normal building - true, - ); - if let Err(err) = block_on(build_fut) { - let err_msg = format!("Static exporting failed: '{}'.", &err); - plugins - .functional_actions - .export_actions - .after_failed_build - .run(err, plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; - } - plugins - .functional_actions - .export_actions - .after_successful_build - .run((), plugins.get_plugin_data()); - // Turn the build artifacts into self-contained static files - let app_root = get_app_root(&plugins); - let export_fut = export_app( - &templates_map, - // Perseus always uses one HTML file, and there's no point in letting a plugin change that - "../index.html", - &locales, - &app_root, - &immutable_store, - &translations_manager, - get_path_prefix_server(), - ); - if let Err(err) = block_on(export_fut) { - let err_msg = format!("Static exporting failed: '{}'.", &err); - plugins - .functional_actions - .export_actions - .after_failed_export - .run(err, plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; - } - - // Copy the `static` directory into the export package if it exists - // If the user wants extra, they can use static aliases, plugins are unnecessary here - let static_dir = PathBuf::from("../static"); - if static_dir.exists() { - if let Err(err) = copy_dir(&static_dir, "dist/exported/.perseus/", &CopyOptions::new()) { - let err_msg = format!( - "Static exporting failed: 'couldn't copy static directory: '{}''", - &err - ); - plugins - .functional_actions - .export_actions - .after_failed_static_copy - .run(err.to_string(), plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; - } - } - // Loop through any static aliases and copy them in too - // Unlike with the server, these could override pages! - // We'll copy from the alias to the path (it could be a directory or a file) - // Remember: `alias` has a leading `/`! - for (alias, path) in get_static_aliases(&plugins) { - let from = PathBuf::from(path); - let to = format!("dist/exported{}", alias); - - if from.is_dir() { - if let Err(err) = copy_dir(&from, &to, &CopyOptions::new()) { - let err_msg = format!( - "Static exporting failed: 'couldn't copy static alias directory: '{}''", - err.to_string() - ); - plugins - .functional_actions - .export_actions - .after_failed_static_alias_dir_copy - .run(err.to_string(), plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; - } - } else if let Err(err) = fs::copy(&from, &to) { - let err_msg = format!( - "Static exporting failed: 'couldn't copy static alias file: '{}''", - err.to_string() - ); - plugins - .functional_actions - .export_actions - .after_failed_static_alias_file_copy - .run(err, plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; - } - } - - plugins - .functional_actions - .export_actions - .after_successful_export - .run((), plugins.get_plugin_data()); - println!("Static exporting successfully completed!"); - 0 -} diff --git a/packages/perseus-cli/.perseus/builder/src/bin/tinker.rs b/packages/perseus-cli/.perseus/builder/src/bin/tinker.rs deleted file mode 100644 index f104ffa569..0000000000 --- a/packages/perseus-cli/.perseus/builder/src/bin/tinker.rs +++ /dev/null @@ -1,22 +0,0 @@ -use perseus::{plugins::PluginAction, SsrNode}; -use perseus_engine::app::get_plugins; - -fn main() { - let exit_code = real_main(); - std::process::exit(exit_code) -} - -fn real_main() -> i32 { - // We want to be working in the root of `.perseus/` - std::env::set_current_dir("../").unwrap(); - - let plugins = get_plugins::(); - // Run all the tinker actions - plugins - .functional_actions - .tinker - .run((), plugins.get_plugin_data()); - - println!("Tinkering complete!"); - 0 -} diff --git a/packages/perseus-cli/.perseus/server/Cargo.toml.old b/packages/perseus-cli/.perseus/server/Cargo.toml.old deleted file mode 100644 index 0253868582..0000000000 --- a/packages/perseus-cli/.perseus/server/Cargo.toml.old +++ /dev/null @@ -1,16 +0,0 @@ -# This crate defines the user's app in terms that Wasm can understand, making development significantly simpler. -# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it! - -[package] -name = "perseus-engine-server" -version = "0.3.0-beta.16" -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -perseus = { path = "../../../../packages/perseus", features = [ "server-side" ] } -perseus-actix-web = { path = "../../../../packages/perseus-actix-web" } -perseus-engine = { path = "../" } -actix-web = "3.3" -futures = "0.3" diff --git a/packages/perseus-cli/.perseus/server/src/main.rs b/packages/perseus-cli/.perseus/server/src/main.rs deleted file mode 100644 index 73ee3ca290..0000000000 --- a/packages/perseus-cli/.perseus/server/src/main.rs +++ /dev/null @@ -1,99 +0,0 @@ -use actix_web::{App, HttpServer}; -use futures::executor::block_on; -use perseus::plugins::PluginAction; -use perseus::SsrNode; -use perseus_actix_web::{configurer, Options}; -use perseus_engine::app::{ - get_app_root, get_error_pages_contained, get_immutable_store, get_locales, get_mutable_store, - get_plugins, get_static_aliases, get_templates_map_contained, get_translations_manager, -}; -use std::collections::HashMap; -use std::env; -use std::fs; - -// This server executable can be run in two modes: -// dev: inside `.perseus/server/src/main.rs`, works with that file structure -// prod: as a standalone executable with a `dist/` directory as a sibling -// The prod mode can be enabled by setting the `PERSEUS_STANDALONE` environment variable - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - let plugins = get_plugins::(); - - // So we don't have to define a different `FsConfigManager` just for the server, we shift the execution context to the same level as everything else - // The server has to be a separate crate because otherwise the dependencies don't work with Wasm bundling - // If we're not running as a standalone binary, assume we're running in dev mode under `.perseus/` - let is_standalone; - if env::var("PERSEUS_STANDALONE").is_err() { - env::set_current_dir("../").unwrap(); - is_standalone = false; - } else { - // If we are running as a standalone binary, we have no idea where we're being executed from (#63), so we should set the working directory to be the same as the binary location - let binary_loc = env::current_exe().unwrap(); - let binary_dir = binary_loc.parent().unwrap(); // It's a file, there's going to be a parent if we're working on anything close to sanity - env::set_current_dir(binary_dir).unwrap(); - is_standalone = true; - } - - plugins - .functional_actions - .server_actions - .before_serve - .run((), plugins.get_plugin_data()); - - // This allows us to operate inside `.perseus/` and as a standalone binary in production - let (html_shell_path, static_dir_path) = if is_standalone { - ("./index.html", "./static") - } else { - ("../index.html", "../static") - }; - - let host = env::var("HOST").unwrap_or_else(|_| "localhost".to_string()); - let port = env::var("PORT") - .unwrap_or_else(|_| "8080".to_string()) - .parse::(); - if let Ok(port) = port { - let immutable_store = get_immutable_store(&plugins); - let locales = get_locales(&plugins); - let app_root = get_app_root(&plugins); - let static_aliases = get_static_aliases(&plugins); - HttpServer::new(move || { - // TODO find a way to configure the server with plugins without using `actix-web` in the `perseus` crate (it won't compile to Wasm) - App::new().configure(block_on(configurer( - Options { - // We don't support setting some attributes from `wasm-pack` through plugins/`define_app!` because that would require CLI changes as well (a job for an alternative engine) - index: html_shell_path.to_string(), // The user must define their own `index.html` file - js_bundle: "dist/pkg/perseus_engine.js".to_string(), - // Our crate has the same name, so this will be predictable - wasm_bundle: "dist/pkg/perseus_engine_bg.wasm".to_string(), - // It's a nightmare to get the templates map to take plugins, so we use a self-contained version - // TODO reduce allocations here - templates_map: get_templates_map_contained(), - locales: locales.clone(), - root_id: app_root.to_string(), - snippets: "dist/pkg/snippets".to_string(), - error_pages: get_error_pages_contained(), - // 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_dir_path).is_ok() { - let mut static_dirs = HashMap::new(); - static_dirs.insert("".to_string(), static_dir_path.to_string()); - static_dirs - } else { - HashMap::new() - }, - static_aliases: static_aliases.clone(), - }, - immutable_store.clone(), - get_mutable_store(), - block_on(get_translations_manager()), - ))) - }) - .bind((host, port))? - .run() - .await - } else { - eprintln!("Port must be a number."); - Ok(()) - } -} diff --git a/packages/perseus-cli/.perseus/src/app.rs b/packages/perseus-cli/.perseus/src/app.rs deleted file mode 100644 index 65780d24ff..0000000000 --- a/packages/perseus-cli/.perseus/src/app.rs +++ /dev/null @@ -1,144 +0,0 @@ -// This file is used for processing data from the `define_app!` macro -// It also applies plugin opportunities for changing aspects thereof - -pub use app::get_plugins; -use perseus::{ - internal::i18n::Locales, stores::ImmutableStore, templates::TemplateMap, ErrorPages, - GenericNode, PluginAction, Plugins, -}; -use std::collections::HashMap; - -pub use app::{get_mutable_store, get_translations_manager}; - -// These functions all take plugins so we don't have to perform possibly very costly allocation more than once in an environment (e.g. browser, build process, export process, server) - -// pub fn get_mutable_store() -> impl MutableStore { -// todo!() -// } -pub fn get_immutable_store(plugins: &Plugins) -> ImmutableStore { - let immutable_store = app::get_immutable_store(); - plugins - .control_actions - .settings_actions - .set_immutable_store - .run(immutable_store.clone(), plugins.get_plugin_data()) - .unwrap_or(immutable_store) -} -pub fn get_app_root(plugins: &Plugins) -> String { - plugins - .control_actions - .settings_actions - .set_app_root - .run((), plugins.get_plugin_data()) - .unwrap_or_else(|| app::APP_ROOT.to_string()) -} -// pub async fn get_translations_manager() -> impl TranslationsManager { -// todo!() -// } -pub fn get_locales(plugins: &Plugins) -> Locales { - let locales = app::get_locales(); - plugins - .control_actions - .settings_actions - .set_locales - .run(locales.clone(), plugins.get_plugin_data()) - .unwrap_or(locales) -} -// This also performs rescoping and security checks so that we don't include anything outside the project root -pub fn get_static_aliases(plugins: &Plugins) -> HashMap { - let mut static_aliases = app::get_static_aliases(); - // This will return a map of plugin name to another map of static aliases that that plugin produced - let extra_static_aliases = plugins - .functional_actions - .settings_actions - .add_static_aliases - .run((), plugins.get_plugin_data()); - for (_plugin_name, aliases) in extra_static_aliases { - let new_aliases: HashMap = aliases - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - static_aliases.extend(new_aliases); - } - - let mut scoped_static_aliases = HashMap::new(); - for (url, path) in static_aliases { - // 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 new_path = if path.starts_with('/') { - // Absolute paths are a security risk and are disallowed - panic!( - "it's a security risk to include absolute paths in `static_aliases` ('{}')", - path - ); - } else if path.starts_with("../") { - // Anything outside this directory is a security risk as well - panic!("it's a security risk to include paths outside the current directory in `static_aliases` ('{}')", path); - } else if path.starts_with("./") { - // `./` -> `../` (moving to execution from `.perseus/`) - // But if we're operating standalone, it stays the same - if ::std::env::var("PERSEUS_STANDALONE").is_ok() { - path.to_string() - } else { - format!(".{}", path) - } - } else { - // Anything else gets a `../` prepended - // But if we're operating standalone, it stays the same - if ::std::env::var("PERSEUS_STANDALONE").is_ok() { - path.to_string() - } else { - format!("../{}", path) - } - }; - - scoped_static_aliases.insert(url, new_path); - } - - scoped_static_aliases -} -// This doesn't take plugins because that would actually increase allocation and indirection on the server -pub fn get_templates_map(plugins: &Plugins) -> TemplateMap { - let mut templates = app::get_templates_map::(); - // This will return a map of plugin name to a vector of templates to add - let extra_templates = plugins - .functional_actions - .settings_actions - .add_templates - .run((), plugins.get_plugin_data()); - for (_plugin_name, plugin_templates) in extra_templates { - // Turn that vector into a template map by extracting the template root paths as keys - for template in plugin_templates { - templates.insert(template.get_path(), template); - } - } - - templates -} -pub fn get_error_pages(plugins: &Plugins) -> ErrorPages { - let mut error_pages = app::get_error_pages::(); - // This will return a map of plugin name to a map of status codes to error pages - let extra_error_pages = plugins - .functional_actions - .settings_actions - .add_error_pages - .run((), plugins.get_plugin_data()); - for (_plugin_name, plugin_error_pages) in extra_error_pages { - for (status, error_page) in plugin_error_pages { - error_pages.add_page_rc(status, error_page); - } - } - - error_pages -} - -// We provide alternatives for `get_templates_map` and `get_error_pages` that get their own plugins -// This avoids major allocation/sync problems on the server -pub fn get_templates_map_contained() -> TemplateMap { - let plugins = get_plugins::(); - get_templates_map(&plugins) -} -pub fn get_error_pages_contained() -> ErrorPages { - let plugins = get_plugins::(); - get_error_pages(&plugins) -} diff --git a/packages/perseus-cli/.perseus/src/lib.rs b/packages/perseus-cli/.perseus/src/lib.rs deleted file mode 100644 index 2c3646ed9b..0000000000 --- a/packages/perseus-cli/.perseus/src/lib.rs +++ /dev/null @@ -1,145 +0,0 @@ -pub mod app; - -use crate::app::{get_app_root, get_error_pages, get_locales, get_plugins, get_templates_map}; -use perseus::{ - checkpoint, create_app_route, - internal::{ - error_pages::ErrorPageData, - i18n::{detect_locale, ClientTranslationsManager}, - router::{RouteInfo, RouteVerdict}, - shell::{app_shell, get_initial_state, get_render_cfg, InitialState}, - }, - plugins::PluginAction, - DomNode, -}; -use std::cell::RefCell; -use std::rc::Rc; -use sycamore::prelude::{cloned, create_effect, template, NodeRef, StateHandle}; -use sycamore_router::{HistoryIntegration, Router, RouterProps}; -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; - -/// The entrypoint into the app itself. This will be compiled to Wasm and actually executed, rendering the rest of the app. -#[wasm_bindgen] -pub fn run() -> Result<(), JsValue> { - let plugins = get_plugins::(); - - checkpoint("begin"); - // Panics should always go to the console - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - - plugins - .functional_actions - .client_actions - .start - .run((), plugins.get_plugin_data()); - checkpoint("initial_plugins_complete"); - - // Get the root we'll be injecting the router into - let root = web_sys::window() - .unwrap() - .document() - .unwrap() - .query_selector(&format!("#{}", get_app_root(&plugins))) - .unwrap() - .unwrap(); - - // Get the root that the server will have injected initial load content into - // This will be moved into a reactive `
` by the app shell - // This is an `Option` until we know we aren't doing locale detection (in which case it wouldn't exist) - let initial_container = web_sys::window() - .unwrap() - .document() - .unwrap() - .query_selector("#__perseus_content_initial") - .unwrap(); - // And create a node reference that we can use as a handle to the reactive verison - let container_rx = NodeRef::new(); - - // Create a mutable translations manager to control caching - let locales = get_locales(&plugins); - let translations_manager = Rc::new(RefCell::new(ClientTranslationsManager::new(&locales))); - // Get the error pages in an `Rc` so we aren't creating hundreds of them - let error_pages = Rc::new(get_error_pages(&plugins)); - - // Create the router we'll use for this app, based on the user's app definition - create_app_route! { - name => AppRoute, - // The render configuration is injected verbatim into the HTML shell, so it certainly should be present - render_cfg => &get_render_cfg().expect("render configuration invalid or not injected"), - // TODO avoid unnecessary allocation here (major problem!) - // The `G` parameter is ambient here for `RouteVerdict` - templates => &get_templates_map::(&get_plugins()), - locales => &get_locales::(&get_plugins()) - } - - // Put the locales into an `Rc` so we can use them in locale detection (which is inside a future) - let locales = Rc::new(locales); - - sycamore::render_to( - move || { - template! { - Router(RouterProps::new(HistoryIntegration::new(), move |route: StateHandle>| { - create_effect(cloned!((container_rx) => move || { - // Sycamore's reactivity is broken by a future, so we need to explicitly add the route to the reactive dependencies here - // We do need the future though (otherwise `container_rx` doesn't link to anything until it's too late) - let _ = route.get(); - wasm_bindgen_futures::spawn_local(cloned!((locales, route, container_rx, translations_manager, error_pages, initial_container) => async move { - let container_rx_elem = container_rx.get::().unchecked_into::(); - checkpoint("router_entry"); - match &route.get().as_ref().0 { - // Perseus' custom routing system is tightly coupled to the template system, and returns exactly what we need for the app shell! - // If a non-404 error occurred, it will be handled in the app shell - RouteVerdict::Found(RouteInfo { - path, - template, - locale, - was_incremental_match - }) => app_shell( - path.clone(), - (template.clone(), *was_incremental_match), - locale.clone(), - // We give the app shell a translations manager and let it get the `Rc` itself (because it can do async safely) - Rc::clone(&translations_manager), - Rc::clone(&error_pages), - (initial_container.unwrap().clone(), container_rx_elem.clone()) - ).await, - // If the user is using i18n, then they'll want to detect the locale on any paths missing a locale - // Those all go to the same system that redirects to the appropriate locale - // Note that `container` doesn't exist for this scenario - RouteVerdict::LocaleDetection(path) => detect_locale(path.clone(), &locales), - // To get a translator here, we'd have to go async and dangerously check the URL - // If this is an initial load, there'll already be an error message, so we should only proceed if the declaration is not `error` - RouteVerdict::NotFound => { - checkpoint("not_found"); - if let InitialState::Error(ErrorPageData { url, status, err }) = get_initial_state() { - let initial_container = initial_container.unwrap(); - // We need to move the server-rendered content from its current container to the reactive container (otherwise Sycamore can't work with it properly) - let initial_html = initial_container.inner_html(); - container_rx_elem.set_inner_html(&initial_html); - initial_container.set_inner_html(""); - // Make the initial container invisible - initial_container.set_attribute("style", "display: none;").unwrap(); - // Hydrate the error pages - // Right now, we don't provide translators to any error pages that have come from the server - error_pages.hydrate_page(&url, &status, &err, None, &container_rx_elem); - } else { - error_pages.hydrate_page("", &404, "not found", None, &container_rx_elem); - } - }, - }; - })); - })); - // This template is reactive, and will be updated as necessary - // However, the server has already rendered initial load content elsewhere, so we move that into here as well in the app shell - // The main reason for this is that the router only intercepts click events from its children - template! { - div(id="__perseus_content_rx", class="__perseus_content", ref=container_rx) {} - } - })) - } - }, - &root, - ); - - Ok(()) -}