├── templates ├── default │ ├── crates │ │ ├── stctl │ │ │ ├── src │ │ │ │ ├── lib.rs │ │ │ │ └── bin │ │ │ │ │ └── stctl.rs │ │ │ └── Cargo.toml.liquid │ │ ├── view │ │ │ ├── src │ │ │ │ ├── pages │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── server_time.rs │ │ │ │ │ └── greeting.rs │ │ │ │ └── lib.rs.liquid │ │ │ └── Cargo.toml.liquid │ │ ├── .gitignore │ │ ├── api │ │ │ ├── src │ │ │ │ ├── lib.rs │ │ │ │ ├── resolvers.rs │ │ │ │ └── routines.rs │ │ │ └── Cargo.toml.liquid │ │ ├── client │ │ │ ├── src │ │ │ │ ├── app.rs │ │ │ │ └── main.rs.liquid │ │ │ └── Cargo.toml.liquid │ │ └── server │ │ │ ├── src │ │ │ ├── bridge.rs │ │ │ ├── app.rs │ │ │ └── main.rs.liquid │ │ │ └── Cargo.toml.liquid │ ├── cargo-generate.toml │ ├── Cargo.toml.liquid │ ├── stellation.toml.liquid │ ├── README.md │ ├── index.html.liquid │ ├── .gitignore │ ├── rustfmt.toml │ ├── Makefile.toml │ └── resolve-crates.rhai ├── cargo-generate.toml └── README.md ├── artworks └── quickstart.gif ├── crates ├── stctl │ ├── src │ │ ├── bin │ │ │ └── stctl.rs │ │ ├── manifest.rs │ │ ├── utils.rs │ │ ├── profile.rs │ │ ├── indicators.rs │ │ ├── env_file.rs │ │ ├── cli.rs │ │ └── paths.rs │ └── Cargo.toml ├── stellation-backend │ ├── src │ │ ├── utils │ │ │ ├── mod.rs │ │ │ └── thread_local.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── request.rs │ │ ├── props.rs │ │ ├── html.rs │ │ ├── hooks.rs │ │ ├── root.rs │ │ └── renderer.rs │ └── Cargo.toml ├── stellation-frontend │ ├── src │ │ ├── components │ │ │ ├── mod.rs │ │ │ └── client_only.rs │ │ ├── trace.rs │ │ ├── root.rs │ │ └── lib.rs │ └── Cargo.toml ├── stellation │ ├── src │ │ ├── guides │ │ │ ├── mod.rs │ │ │ ├── migration_02_03.rs │ │ │ └── migration_01_02.rs │ │ └── lib.rs │ └── Cargo.toml ├── stellation-bridge │ ├── src │ │ ├── registry │ │ │ ├── mod.rs │ │ │ ├── resolver.rs │ │ │ └── routine.rs │ │ ├── hooks │ │ │ ├── mod.rs │ │ │ ├── use_bridged_query.rs │ │ │ ├── use_bridged_mutation.rs │ │ │ └── use_bridged_query_value.rs │ │ ├── error.rs │ │ ├── links │ │ │ ├── mod.rs │ │ │ ├── phantom_link.rs │ │ │ ├── local_link.rs │ │ │ └── fetch_link.rs │ │ ├── resolvers.rs │ │ ├── lib.rs │ │ ├── bridge.rs │ │ ├── routines.rs │ │ └── state.rs │ └── Cargo.toml ├── stellation-core │ ├── src │ │ ├── lib.rs │ │ └── dev.rs │ └── Cargo.toml ├── stellation-backend-cli │ ├── src │ │ ├── lib.rs │ │ ├── cli.rs │ │ └── trace.rs │ └── Cargo.toml ├── stellation-backend-warp │ ├── src │ │ ├── lib.rs │ │ ├── utils.rs │ │ ├── html.rs │ │ ├── filters.rs │ │ ├── request.rs │ │ └── frontend.rs │ └── Cargo.toml ├── stellation-stylist │ ├── src │ │ ├── lib.rs │ │ ├── frontend.rs │ │ └── backend.rs │ └── Cargo.toml └── stellation-backend-tower │ ├── src │ ├── lib.rs │ ├── endpoint.rs │ └── server.rs │ └── Cargo.toml ├── examples └── fullstack │ ├── view │ ├── src │ │ ├── pages │ │ │ ├── mod.rs │ │ │ ├── server_time.rs │ │ │ └── greeting.rs │ │ └── lib.rs │ └── Cargo.toml │ ├── .gitignore │ ├── stellation.toml │ ├── api │ ├── src │ │ ├── lib.rs │ │ ├── resolvers.rs │ │ └── routines.rs │ └── Cargo.toml │ ├── index.html │ ├── client │ ├── src │ │ ├── app.rs │ │ └── main.rs │ └── Cargo.toml │ └── server │ ├── src │ ├── bridge.rs │ ├── app.rs │ └── main.rs │ └── Cargo.toml ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── Cargo.toml ├── rustfmt.toml ├── Makefile.toml ├── ci ├── switch-registry.py └── update-version.py ├── LICENSE-MIT └── README.md /templates/default/crates/stctl/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/cargo-generate.toml: -------------------------------------------------------------------------------- 1 | [template] 2 | sub_templates = ["default"] 3 | -------------------------------------------------------------------------------- /artworks/quickstart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futursolo/stellation/HEAD/artworks/quickstart.gif -------------------------------------------------------------------------------- /crates/stctl/src/bin/stctl.rs: -------------------------------------------------------------------------------- 1 | #[tokio::main] 2 | async fn main() -> anyhow::Result<()> { 3 | stctl::main().await 4 | } 5 | -------------------------------------------------------------------------------- /templates/default/cargo-generate.toml: -------------------------------------------------------------------------------- 1 | [template] 2 | cargo_generate_version = ">=0.9.0" 3 | 4 | [hooks] 5 | init = ["resolve-crates.rhai"] 6 | -------------------------------------------------------------------------------- /crates/stellation-backend/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! Server utilities. 2 | 3 | mod thread_local; 4 | pub use self::thread_local::ThreadLocalLazy; 5 | -------------------------------------------------------------------------------- /examples/fullstack/view/src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | mod greeting; 2 | mod server_time; 3 | 4 | pub use greeting::Greeting; 5 | pub use server_time::ServerTime; 6 | -------------------------------------------------------------------------------- /crates/stellation-frontend/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | //! Stellation helper components. 2 | 3 | mod client_only; 4 | 5 | pub use client_only::ClientOnly; 6 | -------------------------------------------------------------------------------- /templates/default/crates/view/src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | mod greeting; 2 | mod server_time; 3 | 4 | pub use greeting::Greeting; 5 | pub use server_time::ServerTime; 6 | -------------------------------------------------------------------------------- /examples/fullstack/.gitignore: -------------------------------------------------------------------------------- 1 | # Files generated by Cargo 2 | target/ 3 | 4 | # These are backup files generated by rustfmt 5 | **/*.rs.bk 6 | 7 | # Files created by Stellation 8 | .stellation/ 9 | -------------------------------------------------------------------------------- /templates/default/crates/.gitignore: -------------------------------------------------------------------------------- 1 | # Files generated by Cargo 2 | target/ 3 | 4 | # These are backup files generated by rustfmt 5 | **/*.rs.bk 6 | 7 | # Files created by Stellation 8 | .stellation/ 9 | -------------------------------------------------------------------------------- /templates/default/Cargo.toml.liquid: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/*", 4 | ] 5 | resolver = "2" 6 | 7 | [profile.release] 8 | lto = true 9 | codegen-units = 1 10 | panic = "abort" 11 | opt-level = "z" 12 | -------------------------------------------------------------------------------- /examples/fullstack/stellation.toml: -------------------------------------------------------------------------------- 1 | # Configures development server 2 | [dev-server] 3 | # The binary name of server 4 | bin-name = "example-fullstack-server" 5 | # The address that the development server listens to 6 | listen = "localhost:5000" 7 | -------------------------------------------------------------------------------- /templates/README.md: -------------------------------------------------------------------------------- 1 | # Stellation Templates 2 | 3 | To create a stellation project from one of the templates, use the following command: 4 | 5 | ```bash 6 | $ cargo generate --git https://github.com/futursolo/stellation-templates 7 | ``` 8 | -------------------------------------------------------------------------------- /templates/default/stellation.toml.liquid: -------------------------------------------------------------------------------- 1 | # Configures development server 2 | [dev-server] 3 | # The binary name of server 4 | bin-name = "{{project-name}}-server" 5 | # The address that the development server listens to 6 | listen = "localhost:5000" 7 | -------------------------------------------------------------------------------- /templates/default/crates/stctl/src/bin/stctl.rs: -------------------------------------------------------------------------------- 1 | // A vendored version of stctl, this is a workaround until https://github.com/rust-lang/rfcs/pull/3168 is implemented. 2 | #[tokio::main] 3 | async fn main() -> anyhow::Result<()> { 4 | stctl::main().await 5 | } 6 | -------------------------------------------------------------------------------- /templates/default/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Stellation 2 | 3 | This project is created with Stellation. 4 | 5 | # Available Commands 6 | 7 | To start the development server, use `cargo make start`. 8 | To build a release distribuation, use `cargo make build`. 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | 9 | - package-ecosystem: "cargo" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | -------------------------------------------------------------------------------- /examples/fullstack/api/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(missing_debug_implementations)] 3 | 4 | #[cfg(feature = "resolvable")] 5 | mod resolvers; 6 | mod routines; 7 | 8 | #[cfg(feature = "resolvable")] 9 | pub use resolvers::*; 10 | #[cfg(not(feature = "resolvable"))] 11 | pub use routines::*; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /crates/*/target 5 | /examples/*/target 6 | /examples/*/build 7 | 8 | # These are backup files generated by rustfmt 9 | **/*.rs.bk 10 | 11 | # Visual Studio Code 12 | .vscode/ 13 | 14 | # Pyenv 15 | .python-version 16 | -------------------------------------------------------------------------------- /templates/default/crates/api/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(missing_debug_implementations)] 3 | 4 | #[cfg(feature = "resolvable")] 5 | mod resolvers; 6 | mod routines; 7 | 8 | #[cfg(feature = "resolvable")] 9 | pub use resolvers::*; 10 | #[cfg(not(feature = "resolvable"))] 11 | pub use routines::*; 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/*", 4 | "examples/fullstack/*", 5 | ] 6 | exclude = [ 7 | "examples/fullstack/.stellation", 8 | "examples/fullstack/build", 9 | ] 10 | resolver = "2" 11 | 12 | [profile.release] 13 | lto = true 14 | codegen-units = 1 15 | panic = "abort" 16 | opt-level = "z" 17 | -------------------------------------------------------------------------------- /examples/fullstack/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /crates/stellation/src/guides/mod.rs: -------------------------------------------------------------------------------- 1 | //! Guides and Tutorials 2 | //! 3 | //! This module contains various guides and tutorials for stellation. 4 | //! 5 | //! # Migration Guides 6 | //! 7 | //! 1. [v0.1 to v0.2](migration_01_02) 8 | //! 1. [v0.2 to v0.3](migration_02_03) 9 | 10 | pub mod migration_01_02; 11 | pub mod migration_02_03; 12 | -------------------------------------------------------------------------------- /templates/default/index.html.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/registry/mod.rs: -------------------------------------------------------------------------------- 1 | //! Registries for Routines and their Resolvers. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | mod routine; 6 | pub use routine::*; 7 | 8 | mod resolver; 9 | pub use resolver::*; 10 | 11 | #[derive(Debug, Serialize, Deserialize)] 12 | struct Incoming<'a> { 13 | query_index: usize, 14 | input: &'a [u8], 15 | } 16 | -------------------------------------------------------------------------------- /crates/stctl/src/manifest.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | #[serde(rename_all = "kebab-case")] 5 | pub(crate) struct DevServer { 6 | pub listen: String, 7 | pub bin_name: String, 8 | } 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | #[serde(rename_all = "kebab-case")] 12 | pub(crate) struct Manifest { 13 | pub dev_server: DevServer, 14 | } 15 | -------------------------------------------------------------------------------- /examples/fullstack/client/src/app.rs: -------------------------------------------------------------------------------- 1 | use example_fullstack_view::Main; 2 | use stellation_stylist::FrontendManagerProvider; 3 | use yew::prelude::*; 4 | 5 | #[function_component] 6 | pub fn App() -> Html { 7 | html! { 8 | 9 | 10 |
11 | 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | 3 | format_code_in_doc_comments = true 4 | wrap_comments = true 5 | comment_width = 100 # same as default max_width 6 | normalize_doc_attributes = true 7 | normalize_comments = true 8 | 9 | condense_wildcard_suffixes = true 10 | format_strings = true 11 | group_imports = "StdExternalCrate" 12 | imports_granularity = "Module" 13 | reorder_impl_items = true 14 | use_field_init_shorthand = true 15 | -------------------------------------------------------------------------------- /templates/default/crates/client/src/app.rs: -------------------------------------------------------------------------------- 1 | use stellation_stylist::FrontendManagerProvider; 2 | use yew::prelude::*; 3 | 4 | use crate::view::Main; 5 | 6 | #[function_component] 7 | pub fn App() -> Html { 8 | html! { 9 | 10 | 11 |
12 | 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /crates/stellation-backend/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// The error type returned by server app methods. 4 | #[derive(Error, Debug)] 5 | pub enum ServerAppError { 6 | /// failed to parse queries. 7 | #[error("failed to parse queries")] 8 | Queries(#[from] serde_urlencoded::de::Error), 9 | } 10 | 11 | /// The result type returned by server app methods. 12 | pub type ServerAppResult = Result; 13 | -------------------------------------------------------------------------------- /templates/default/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # Stellation artifacts 13 | .stellation/ 14 | build/ 15 | -------------------------------------------------------------------------------- /templates/default/rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | 3 | format_code_in_doc_comments = true 4 | wrap_comments = true 5 | comment_width = 100 # same as default max_width 6 | normalize_doc_attributes = true 7 | normalize_comments = true 8 | 9 | condense_wildcard_suffixes = true 10 | format_strings = true 11 | group_imports = "StdExternalCrate" 12 | imports_granularity = "Module" 13 | reorder_impl_items = true 14 | use_field_init_shorthand = true 15 | -------------------------------------------------------------------------------- /examples/fullstack/server/src/bridge.rs: -------------------------------------------------------------------------------- 1 | use example_fullstack_api::{create_resolver_registry, create_routine_registry, Bridge, Link}; 2 | use stellation_backend_tower::TowerRequest; 3 | 4 | pub async fn create_backend_bridge(_req: TowerRequest<()>) -> Bridge { 5 | Bridge::new( 6 | Link::builder() 7 | .context(()) 8 | .resolvers(create_resolver_registry()) 9 | .routines(create_routine_registry()) 10 | .build(), 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /templates/default/crates/server/src/bridge.rs: -------------------------------------------------------------------------------- 1 | use stellation_backend_tower::TowerRequest; 2 | 3 | use crate::api::{create_resolver_registry, create_routine_registry, Bridge, Link}; 4 | 5 | pub async fn create_backend_bridge(_req: TowerRequest<()>) -> Bridge { 6 | Bridge::new( 7 | Link::builder() 8 | .context(()) 9 | .resolvers(create_resolver_registry()) 10 | .routines(create_routine_registry()) 11 | .build(), 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /crates/stellation-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The core component of Stellation, shared between other crates. 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_debug_implementations)] 5 | #![deny(unsafe_code)] 6 | #![deny(non_snake_case)] 7 | #![deny(clippy::cognitive_complexity)] 8 | #![deny(missing_docs)] 9 | #![cfg_attr(documenting, feature(doc_cfg))] 10 | #![cfg_attr(documenting, feature(doc_auto_cfg))] 11 | #![cfg_attr(any(releasing, not(debug_assertions)), deny(dead_code, unused_imports))] 12 | 13 | pub mod dev; 14 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true 3 | 4 | # stctl 5 | [tasks.stctl] 6 | workspace = false 7 | command = "cargo" 8 | args = ["run", "--bin", "stctl", "--", "${@}"] 9 | 10 | [tasks.clippy] 11 | clear = true 12 | workspace = false 13 | script = ''' 14 | #!/usr/bin/env bash 15 | set -e 16 | 17 | cargo clippy --workspace --all-features -- -D warnings 18 | 19 | cargo clippy -p example-fullstack-client -- -D warnings 20 | cargo clippy -p example-fullstack-server -- -D warnings 21 | ''' 22 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/hooks/mod.rs: -------------------------------------------------------------------------------- 1 | //! Hooks used to resolve requests. 2 | 3 | mod use_bridged_mutation; 4 | mod use_bridged_query; 5 | mod use_bridged_query_value; 6 | 7 | pub use use_bridged_mutation::{ 8 | use_bridged_mutation, BridgedMutationState, UseBridgedMutationHandle, 9 | }; 10 | pub use use_bridged_query::{use_bridged_query, BridgedQueryState, UseBridgedQueryHandle}; 11 | pub use use_bridged_query_value::{ 12 | use_bridged_query_value, BridgedQueryValueState, UseBridgedQueryValueHandle, 13 | }; 14 | -------------------------------------------------------------------------------- /examples/fullstack/client/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(missing_debug_implementations)] 3 | 4 | mod app; 5 | use app::App; 6 | use example_fullstack_api::FrontendBridge; 7 | use tracing_subscriber::filter::LevelFilter; 8 | 9 | fn main() { 10 | // Configures Logging 11 | stellation_frontend::trace::init_default(LevelFilter::INFO); 12 | 13 | // Starts Application 14 | stellation_frontend::Renderer::::new() 15 | .bridge_selector::() 16 | .render(); 17 | } 18 | -------------------------------------------------------------------------------- /crates/stellation-backend-cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Stellation Backend Command Line Utility. 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_debug_implementations)] 5 | #![deny(unsafe_code)] 6 | #![deny(non_snake_case)] 7 | #![deny(clippy::cognitive_complexity)] 8 | #![deny(missing_docs)] 9 | #![cfg_attr(documenting, feature(doc_cfg))] 10 | #![cfg_attr(documenting, feature(doc_auto_cfg))] 11 | #![cfg_attr(any(releasing, not(debug_assertions)), deny(dead_code, unused_imports))] 12 | 13 | mod cli; 14 | pub mod trace; 15 | pub use cli::Cli; 16 | -------------------------------------------------------------------------------- /templates/default/Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true 3 | CARGO_MAKE_CLIPPY_ARGS = "--all-features -- -D warnings" 4 | 5 | # stctl 6 | [tasks.stctl] 7 | workspace = false 8 | command = "cargo" 9 | args = ["run", "--bin", "stctl", "--", "${@}"] 10 | 11 | [tasks.start] 12 | workspace = false 13 | command = "cargo" 14 | args = ["run", "--bin", "stctl", "--", "serve", "--open"] 15 | 16 | [tasks.build] 17 | workspace = false 18 | command = "cargo" 19 | args = ["run", "--bin", "stctl", "--", "build", "--release"] 20 | -------------------------------------------------------------------------------- /crates/stctl/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | use anyhow::Result; 4 | use rand::distributions::Alphanumeric; 5 | use rand::Rng; 6 | 7 | pub(crate) fn random_str() -> Result { 8 | let s: String = rand::thread_rng() 9 | .sample_iter(&Alphanumeric) 10 | .take(7) 11 | .map(char::from) 12 | .collect(); 13 | 14 | Ok(format!( 15 | "{}-{}", 16 | SystemTime::now() 17 | .duration_since(SystemTime::UNIX_EPOCH)? 18 | .as_secs(), 19 | s 20 | )) 21 | } 22 | -------------------------------------------------------------------------------- /examples/fullstack/server/src/app.rs: -------------------------------------------------------------------------------- 1 | use example_fullstack_view::Main; 2 | use stellation_backend::{Request, ServerAppProps}; 3 | use stellation_stylist::BackendManagerProvider; 4 | use yew::prelude::*; 5 | 6 | #[function_component] 7 | pub fn ServerApp(_props: &ServerAppProps<(), REQ>) -> Html 8 | where 9 | REQ: Request, 10 | { 11 | html! { 12 | 13 | 14 |
15 | 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /templates/default/crates/server/src/app.rs: -------------------------------------------------------------------------------- 1 | use stellation_backend::{Request, ServerAppProps}; 2 | use stellation_stylist::BackendManagerProvider; 3 | use yew::prelude::*; 4 | 5 | use crate::view::Main; 6 | 7 | #[function_component] 8 | pub fn ServerApp(_props: &ServerAppProps<(), REQ>) -> Html 9 | where 10 | REQ: Request, 11 | { 12 | html! { 13 | 14 | 15 |
16 | 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/fullstack/api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-fullstack-api" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | stellation-bridge = { path = "../../../crates/stellation-bridge" } 11 | time = { version = "0.3", features = ["wasm-bindgen", "serde-human-readable"] } 12 | serde = { version = "1", features = ["derive"] } 13 | async-trait = "0.1.73" 14 | thiserror = "1" 15 | bounce = "0.8" 16 | 17 | [features] 18 | resolvable = [] 19 | -------------------------------------------------------------------------------- /templates/default/crates/client/src/main.rs.liquid: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(missing_debug_implementations)] 3 | 4 | use {{crate_name}}_api as api; 5 | use {{crate_name}}_view as view; 6 | 7 | mod app; 8 | use app::App; 9 | use api::FrontendBridge; 10 | use tracing_subscriber::filter::LevelFilter; 11 | 12 | fn main() { 13 | // Configures Logging 14 | stellation_frontend::trace::init_default(LevelFilter::INFO); 15 | 16 | // Starts Application 17 | stellation_frontend::Renderer::::new() 18 | .bridge_selector::() 19 | .render(); 20 | } 21 | -------------------------------------------------------------------------------- /templates/default/resolve-crates.rhai: -------------------------------------------------------------------------------- 1 | 2 | //! Resolves stellation crates 3 | //! 4 | //! This template has three targets: 5 | //! 6 | //! - `main`: the template targets the main branch. 7 | //! - `release`: the template targets the release set in stellation_release_ver. 8 | //! - `custom`: the template will exclude stellation crates, they should be added manually. 9 | //! - `ci`: the template is running under GitHub Actions to verify the template. 10 | 11 | // Sets the default target to "main" 12 | variable::set("stellation_target", "main"); 13 | variable::set("stellation_release_ver", ""); 14 | 15 | // Additional definition 16 | -------------------------------------------------------------------------------- /crates/stellation/src/guides/migration_02_03.rs: -------------------------------------------------------------------------------- 1 | //! Migration Guide from v0.2 to v0.3 2 | //! 3 | //! # 1. Bounce is updated to version v0.8 4 | //! 5 | //! # 2. bridged mutations and queries now exposes their state. 6 | //! 7 | //! Previously, mutations and queries exposes 2 methods on their handle `result()` and `status()`. 8 | //! The status returns an enum where it can only be used to check the status of the query / 9 | //! mutation. In Bounce v0.8, it has been modified to return a `State` Enum where the completed and 10 | //! outdated variant contains the query / mutation result. The stellation bridge has been updated to 11 | //! follow this new convention. 12 | -------------------------------------------------------------------------------- /templates/default/crates/stctl/Cargo.toml.liquid: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{project-name}}-stctl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | tokio = "1" 11 | anyhow = "1" 12 | 13 | {% if stellation_target == "release" %} 14 | # Stellation 15 | stctl = "{{stellation_release_ver}}" 16 | {% elsif stellation_target == "main" %} 17 | # Stellation 18 | stctl = { git = "https://github.com/futursolo/stellation" } 19 | {% elsif stellation_target == "ci" %} 20 | # Stellation 21 | stctl = { path = "../../../../stellation/crates/stctl" } 22 | {% endif %} 23 | -------------------------------------------------------------------------------- /crates/stellation-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellation-core" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.66" 6 | repository = "https://github.com/futursolo/stellation" 7 | authors = ["Kaede Hoshiakwa "] 8 | description = "The framework experience for Yew." 9 | keywords = ["web", "wasm", "yew", "framework", "ssr"] 10 | categories = ["wasm", "web-programming"] 11 | readme = "../../README.md" 12 | homepage = "https://github.com/futursolo/stellation" 13 | license = "MIT OR Apache-2.0" 14 | 15 | [dependencies] 16 | serde = { version = "1", features = ["derive"] } 17 | serde_json = "1.0.105" 18 | 19 | [package.metadata.docs.rs] 20 | all-features = true 21 | rustdoc-args = ["--cfg", "documenting"] 22 | -------------------------------------------------------------------------------- /examples/fullstack/view/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-fullstack-view" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | yew = { version = "0.20.0" } 11 | example-fullstack-api = { path = "../api" } 12 | stellation-bridge = { path = "../../../crates/stellation-bridge" } 13 | time = { version = "0.3", features = [ 14 | "wasm-bindgen", 15 | "serde-human-readable", 16 | "macros", 17 | ] } 18 | tracing = "0.1.37" 19 | bounce = { version = "0.8.0", features = ["helmet"] } 20 | stylist = { version = "0.12.1", features = ["yew_integration"] } 21 | 22 | [dependencies.web-sys] 23 | version = "0.3" 24 | features = ["HtmlInputElement"] 25 | -------------------------------------------------------------------------------- /crates/stellation-frontend/src/components/client_only.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use yew::html::ChildrenProps; 4 | use yew::prelude::*; 5 | 6 | /// A component that automatically excludes its children from server-side rendering. 7 | #[function_component] 8 | pub fn ClientOnly(props: &ChildrenProps) -> Html { 9 | let should_render = use_state(|| false); 10 | 11 | // Effects are only run on the client side. 12 | { 13 | use_effect_with_deps( 14 | |should_render_setter| { 15 | should_render_setter.set(true); 16 | }, 17 | should_render.setter(), 18 | ); 19 | } 20 | 21 | match should_render.deref() { 22 | true => html! {<>{props.children.clone()}}, 23 | false => Html::default(), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/stellation/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Framework experience for Yew. 2 | //! 3 | //! Stellation provides a Yew development stack with: 4 | //! 5 | //! 1. Tooling around Server-side Rendering Support. 6 | //! 2. An easy-to-use, SSR-transparent RPC Implementation. 7 | //! 3. A development server that automatically rebuild upon changes. 8 | //! 4. A single binary distribution with embedded frontend. 9 | //! 10 | //! # Components 11 | //! 12 | //! 1. [stellation-frontend](stellation_frontend): The frontend application. 13 | //! 2. [stellation-backend](stellation_backend): The backend server. 14 | //! 3. [stellation-bridge](stellation_bridge): The bridge that provides communication between 15 | //! frontend and backend. 16 | //! 4. [stctl]: Command line utility for building and serving development 17 | //! server. 18 | 19 | pub mod guides; 20 | -------------------------------------------------------------------------------- /crates/stellation-backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Stellation Backend 2 | //! 3 | //! This crate contains the server renderer and tools used for server-side rendering. 4 | 5 | #![deny(clippy::all)] 6 | #![deny(missing_debug_implementations)] 7 | #![deny(unsafe_code)] 8 | #![deny(non_snake_case)] 9 | #![deny(clippy::cognitive_complexity)] 10 | #![deny(missing_docs)] 11 | #![cfg_attr(documenting, feature(doc_cfg))] 12 | #![cfg_attr(documenting, feature(doc_auto_cfg))] 13 | #![cfg_attr(any(releasing, not(debug_assertions)), deny(dead_code, unused_imports))] 14 | 15 | mod error; 16 | mod props; 17 | mod root; 18 | pub mod utils; 19 | pub use error::{ServerAppError, ServerAppResult}; 20 | pub use props::ServerAppProps; 21 | mod request; 22 | pub use request::{RenderRequest, Request}; 23 | mod renderer; 24 | pub use renderer::ServerRenderer; 25 | pub mod hooks; 26 | mod html; 27 | -------------------------------------------------------------------------------- /examples/fullstack/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-fullstack-client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | yew = "0.20.0" 11 | gloo = "0.10" 12 | 13 | # Stellation Components 14 | stellation-frontend = { path = "../../../crates/stellation-frontend" } 15 | stellation-stylist = { path = "../../../crates/stellation-stylist", features = [ 16 | "frontend", 17 | ] } 18 | 19 | # Logging 20 | tracing = "0.1" 21 | tracing-subscriber = { version = "0.3.17", default-features = false, features = [ 22 | "time", 23 | "std", 24 | "fmt", 25 | "ansi", 26 | ] } 27 | 28 | # Example Workspace 29 | example-fullstack-view = { path = "../view" } 30 | example-fullstack-api = { path = "../api" } 31 | -------------------------------------------------------------------------------- /crates/stellation-backend-warp/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Stellation's wrap support. 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_debug_implementations)] 5 | #![deny(unsafe_code)] 6 | #![deny(non_snake_case)] 7 | #![deny(clippy::cognitive_complexity)] 8 | #![deny(missing_docs)] 9 | #![cfg_attr(documenting, feature(doc_cfg))] 10 | #![cfg_attr(documenting, feature(doc_auto_cfg))] 11 | #![cfg_attr(any(releasing, not(debug_assertions)), deny(dead_code, unused_imports))] 12 | 13 | mod endpoint; 14 | mod filters; 15 | mod frontend; 16 | mod html; 17 | mod request; 18 | mod utils; 19 | 20 | pub use endpoint::WarpEndpoint; 21 | pub use frontend::Frontend; 22 | use once_cell::sync::Lazy; 23 | pub use request::{WarpRenderRequest, WarpRequest}; 24 | 25 | // A server id that is different every time it starts. 26 | static SERVER_ID: Lazy = Lazy::new(crate::utils::random_str); 27 | -------------------------------------------------------------------------------- /crates/stctl/src/profile.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub(crate) struct Profile { 3 | name: String, 4 | } 5 | 6 | impl Default for Profile { 7 | fn default() -> Self { 8 | Self::new_debug() 9 | } 10 | } 11 | 12 | impl Profile { 13 | pub fn new_debug() -> Self { 14 | Self { 15 | name: "debug".to_string(), 16 | } 17 | } 18 | 19 | pub fn new_release() -> Self { 20 | Self { 21 | name: "release".to_string(), 22 | } 23 | } 24 | 25 | pub fn name(&self) -> &str { 26 | &self.name 27 | } 28 | 29 | pub fn to_profile_argument(&self) -> Option { 30 | match self.name() { 31 | "debug" => None, 32 | "release" => Some("--release".to_string()), 33 | other => Some(format!("--profile={other}")), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/stellation-stylist/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The stylist integration for stellation. 2 | //! 3 | //! You can check out this [example](https://github.com/futursolo/stellation/tree/main/examples/fullstack) for how to use this crate. 4 | 5 | #![deny(clippy::all)] 6 | #![deny(missing_debug_implementations)] 7 | #![deny(unsafe_code)] 8 | #![deny(non_snake_case)] 9 | #![deny(clippy::cognitive_complexity)] 10 | #![deny(missing_docs)] 11 | #![cfg_attr(documenting, feature(doc_cfg))] 12 | #![cfg_attr(documenting, feature(doc_auto_cfg))] 13 | #![cfg_attr(any(releasing, not(debug_assertions)), deny(dead_code, unused_imports))] 14 | 15 | #[cfg(feature = "backend")] 16 | mod backend; 17 | #[cfg(feature = "frontend")] 18 | mod frontend; 19 | 20 | #[cfg(feature = "backend")] 21 | pub use backend::BackendManagerProvider; 22 | #[cfg(feature = "frontend")] 23 | pub use frontend::FrontendManagerProvider; 24 | -------------------------------------------------------------------------------- /ci/switch-registry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import tomlkit 4 | import glob 5 | from pathlib import Path 6 | 7 | def main() -> None: 8 | cwd = Path.cwd() 9 | print(f"Running in {cwd}...") 10 | 11 | for cargo_toml_path in cwd.glob("crates/*/Cargo.toml"): 12 | cfg = tomlkit.loads(cargo_toml_path.open().read()) 13 | print(f"Updating {cargo_toml_path}...") 14 | 15 | for (key, value) in cfg["dependencies"].items(): 16 | if not isinstance(value, dict) or "path" not in value.keys(): 17 | print(f" Skipping {key}...") 18 | continue 19 | 20 | print(f" Updating {key}...") 21 | 22 | value["registry"] = "dry-run" 23 | 24 | with cargo_toml_path.open("w") as f: 25 | f.write(tomlkit.dumps(cfg)) 26 | f.flush() 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::any::TypeId; 2 | 3 | use thiserror::Error; 4 | 5 | /// The bridge error type. 6 | #[derive(Error, Debug)] 7 | pub enum BridgeError { 8 | /// Some network error happened while communicating with the backend. 9 | #[error("failed to communicate with server")] 10 | Network(#[from] gloo_net::Error), 11 | 12 | /// The bridge failed to encode / decode the message from the other side. 13 | #[error("failed to encode / decode content")] 14 | Encoding(#[from] bincode::Error), 15 | 16 | /// The type does not have a valid index. 17 | #[error("failed to find type with index: {}", .0)] 18 | InvalidIndex(usize), 19 | 20 | /// The type is not valid. 21 | #[error("failed to find type: {:?}", .0)] 22 | InvalidType(TypeId), 23 | } 24 | 25 | /// The bridge result type. 26 | pub type BridgeResult = Result; 27 | -------------------------------------------------------------------------------- /crates/stellation-frontend/src/trace.rs: -------------------------------------------------------------------------------- 1 | //! Tracing support. 2 | 3 | use tracing_subscriber::filter::LevelFilter; 4 | use tracing_subscriber::fmt::format::Pretty; 5 | use tracing_subscriber::fmt::time::UtcTime; 6 | use tracing_subscriber::prelude::*; 7 | use tracing_subscriber::util::SubscriberInitExt; 8 | use tracing_web::{performance_layer, MakeConsoleWriter}; 9 | 10 | /// Initialises [`tracing`] with default parameters. 11 | pub fn init_default(min_level: LevelFilter) { 12 | let fmt_layer = tracing_subscriber::fmt::layer() 13 | .with_ansi(false) 14 | .with_timer(UtcTime::rfc_3339()) 15 | .with_writer(MakeConsoleWriter); 16 | let perf_layer = performance_layer().with_details_from_fields(Pretty::default()); 17 | 18 | tracing_subscriber::registry() 19 | .with(fmt_layer) 20 | .with(perf_layer) 21 | .with(min_level) 22 | .init(); 23 | } 24 | -------------------------------------------------------------------------------- /crates/stellation-backend-warp/src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Warp utilities. 2 | 3 | use futures::Future; 4 | use yew::platform::{LocalHandle, Runtime}; 5 | 6 | /// Creates a random string. 7 | pub(crate) fn random_str() -> String { 8 | use rand::distributions::Alphanumeric; 9 | use rand::Rng; 10 | 11 | rand::thread_rng() 12 | .sample_iter(&Alphanumeric) 13 | .take(7) 14 | .map(char::from) 15 | .collect() 16 | } 17 | 18 | pub(crate) fn spawn_pinned_or_local(create_task: F) 19 | where 20 | F: FnOnce() -> Fut, 21 | F: Send + 'static, 22 | Fut: Future + 'static, 23 | { 24 | // We spawn into a local runtime early for higher efficiency. 25 | match LocalHandle::try_current() { 26 | Some(handle) => handle.spawn_local(create_task()), 27 | // TODO: Allow Overriding Runtime with Endpoint. 28 | None => Runtime::default().spawn_pinned(create_task), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /templates/default/crates/api/Cargo.toml.liquid: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{ project-name }}-api" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | time = { version = "0.3", features = ["wasm-bindgen", "serde-human-readable"] } 11 | serde = { version = "1", features = ["derive"] } 12 | async-trait = "0.1.59" 13 | bounce = "0.8" 14 | thiserror = "1"{% if stellation_target == "release" %} 15 | # Stellation 16 | stellation-bridge = "{{ stellation_release_ver }}" 17 | 18 | {% elsif stellation_target == "main" %} 19 | # Stellation 20 | stellation-bridge = { git = "https://github.com/futursolo/stellation" } 21 | 22 | {% elsif stellation_target == "ci" %} 23 | # Stellation 24 | stellation-bridge = { path = "../../../../stellation/crates/stellation-bridge" } 25 | 26 | {% endif %} 27 | 28 | [features] 29 | resolvable = [] 30 | -------------------------------------------------------------------------------- /crates/stellation-stylist/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellation-stylist" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.66" 6 | repository = "https://github.com/futursolo/stellation" 7 | authors = ["Kaede Hoshiakwa "] 8 | description = "The framework experience for Yew." 9 | keywords = ["web", "wasm", "yew", "framework", "ssr"] 10 | categories = ["wasm", "web-programming"] 11 | readme = "../../README.md" 12 | homepage = "https://github.com/futursolo/stellation" 13 | license = "MIT OR Apache-2.0" 14 | 15 | [dependencies] 16 | stylist = { version = "0.12.1", features = ["yew_integration"] } 17 | yew = "0.20" 18 | 19 | # Stellation Components 20 | stellation-backend = { version = "0.3.0", path = "../stellation-backend", optional = true } 21 | 22 | [features] 23 | frontend = ["stylist/hydration"] 24 | backend = ["stylist/ssr", "dep:stellation-backend"] 25 | 26 | [package.metadata.docs.rs] 27 | all-features = true 28 | rustdoc-args = ["--cfg", "documenting"] 29 | -------------------------------------------------------------------------------- /crates/stellation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellation" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.66" 6 | repository = "https://github.com/futursolo/stellation" 7 | authors = ["Kaede Hoshiakwa "] 8 | description = "The framework experience for Yew." 9 | keywords = ["web", "wasm", "yew", "framework", "ssr"] 10 | categories = ["wasm", "web-programming"] 11 | readme = "../../README.md" 12 | homepage = "https://github.com/futursolo/stellation" 13 | license = "MIT OR Apache-2.0" 14 | 15 | [dependencies] 16 | stellation-core = { version = "0.3.0", path = "../stellation-core" } 17 | stellation-frontend = { version = "0.3.0", path = "../stellation-frontend" } 18 | stellation-backend = { version = "0.3.0", path = "../stellation-backend" } 19 | stellation-bridge = { version = "0.3.0", path = "../stellation-bridge" } 20 | stctl = { version = "0.3.0", path = "../stctl" } 21 | 22 | [package.metadata.docs.rs] 23 | all-features = true 24 | rustdoc-args = ["--cfg", "documenting"] 25 | -------------------------------------------------------------------------------- /crates/stellation-bridge/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellation-bridge" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.66" 6 | repository = "https://github.com/futursolo/stellation" 7 | authors = ["Kaede Hoshiakwa "] 8 | description = "The framework experience for Yew." 9 | keywords = ["web", "wasm", "yew", "framework", "ssr"] 10 | categories = ["wasm", "web-programming"] 11 | readme = "../../README.md" 12 | homepage = "https://github.com/futursolo/stellation" 13 | license = "MIT OR Apache-2.0" 14 | 15 | [dependencies] 16 | async-trait = "0.1.73" 17 | serde = { version = "1", features = ["derive"] } 18 | futures = { version = "0.3", default-features = false, features = ["std"] } 19 | bincode = "1.3.3" 20 | gloo-net = "0.4.0" 21 | js-sys = "0.3.64" 22 | thiserror = "1" 23 | bounce = { version = "0.8.0", features = ["query"] } 24 | yew = "0.20.0" 25 | typed-builder = "0.16.0" 26 | 27 | [package.metadata.docs.rs] 28 | all-features = true 29 | rustdoc-args = ["--cfg", "documenting"] 30 | -------------------------------------------------------------------------------- /examples/fullstack/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-fullstack-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | anyhow = "1" 11 | tokio = { version = "1.32.0", features = ["full"] } 12 | tracing = { version = "0.1.37" } 13 | yew = "0.20.0" 14 | 15 | # Stellation Components 16 | stellation-backend = { path = "../../../crates/stellation-backend" } 17 | stellation-backend-tower = { path = "../../../crates/stellation-backend-tower" } 18 | stellation-backend-cli = { path = "../../../crates/stellation-backend-cli" } 19 | stellation-stylist = { path = "../../../crates/stellation-stylist", features = [ 20 | "backend", 21 | ] } 22 | 23 | # Example Workspace 24 | example-fullstack-view = { path = "../view" } 25 | example-fullstack-api = { path = "../api", features = ["resolvable"] } 26 | rust-embed = { version = "8.0.0", features = ["interpolate-folder-path"] } 27 | -------------------------------------------------------------------------------- /examples/fullstack/server/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(missing_debug_implementations)] 3 | 4 | use stellation_backend_cli::Cli; 5 | use stellation_backend_tower::TowerEndpoint; 6 | 7 | mod app; 8 | mod bridge; 9 | 10 | use app::ServerApp; 11 | use bridge::create_backend_bridge; 12 | 13 | #[cfg(stellation_embedded_frontend)] 14 | #[derive(rust_embed::RustEmbed)] 15 | #[folder = "$STELLATION_FRONTEND_BUILD_DIR"] 16 | struct Frontend; 17 | 18 | #[tokio::main] 19 | async fn main() -> anyhow::Result<()> { 20 | // Configures Logging 21 | stellation_backend_cli::trace::init_default("STELLATION_APP_SERVER_LOG"); 22 | 23 | // Creates Endpoint 24 | let endpoint = TowerEndpoint::>::new().with_create_bridge(create_backend_bridge); 25 | 26 | #[cfg(stellation_embedded_frontend)] 27 | let endpoint = 28 | endpoint.with_frontend(stellation_backend_tower::Frontend::new_embedded::()); 29 | 30 | // Starts Server 31 | Cli::builder().endpoint(endpoint).build().run().await?; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stellation 2 | 3 | A framework experience for Yew. 4 | 5 | Stellation provides a development stack with: 6 | 7 | 1. Tooling around Server-side Rendering Support. 8 | 2. An easy-to-use, SSR-transparent RPC Implementation. 9 | 3. A development server that automatically rebuild upon changes. 10 | 4. A single binary distribution with embedded frontend. 11 | 12 | # Quickstart Guide 13 | 14 |

15 | 16 |

17 | 18 | 1. Install required tools 19 | 20 | Stellation uses the following tools: 21 | 22 | - Trunk 23 | - cargo-generate 24 | - cargo-make 25 | 26 | They can be installed with `cargo install trunk cargo-generate cargo-make` 27 | 28 | 2. Create project 29 | 30 | Run `cargo generate futursolo/stellation-templates` and follow the prompt. 31 | 32 | 3. Start development server 33 | 34 | Run `cargo make --quiet start` in the project directory. 35 | 36 | (This may take a couple minutes when the project is building for the first time.) 37 | -------------------------------------------------------------------------------- /crates/stellation-stylist/src/frontend.rs: -------------------------------------------------------------------------------- 1 | use stylist::manager::StyleManager; 2 | use stylist::yew::ManagerProvider; 3 | use yew::html::ChildrenProps; 4 | use yew::prelude::*; 5 | 6 | /// A Stylist [`ManagerProvider`] that hydrates styles from SSR automatically. 7 | /// This provider should be used in the client app instance. 8 | /// 9 | /// # Panics 10 | /// 11 | /// This provider requires a [`BackendManagerProvider`](crate::BackendManagerProvider) to be 12 | /// placed in the server app or hydration will fail. 13 | /// 14 | /// You can check out this [example](https://github.com/futursolo/stellation/blob/main/examples/fullstack/client/src/app.rs) for how to use this provider. 15 | #[function_component] 16 | pub fn FrontendManagerProvider(props: &ChildrenProps) -> Html { 17 | let manager = use_memo( 18 | |_| StyleManager::new().expect("failed to create style manager."), 19 | (), 20 | ) 21 | .as_ref() 22 | .to_owned(); 23 | 24 | html! { 25 | 26 | {props.children.clone()} 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/stellation-core/src/dev.rs: -------------------------------------------------------------------------------- 1 | //! Stellation development server utilities. 2 | 3 | use std::path::PathBuf; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// Development server metadata. 8 | /// 9 | /// This information is passed from stctl to the server when it is started as a development 10 | /// server. 11 | #[derive(Clone, Debug, Serialize, Deserialize)] 12 | pub struct StctlMetadata { 13 | /// The address the dev server should listen to. 14 | pub listen_addr: String, 15 | /// The directory that contains the development build of frontend artifact. 16 | pub frontend_dev_build_dir: PathBuf, 17 | } 18 | 19 | impl StctlMetadata { 20 | /// The environment variable used by metadata. 21 | pub const ENV_NAME: &str = "STCTL_METADATA"; 22 | 23 | /// Parses the metadata from a json string. 24 | pub fn from_json(s: &str) -> serde_json::Result { 25 | serde_json::from_str(s) 26 | } 27 | 28 | /// Serialises the metadata to a json string. 29 | pub fn to_json(&self) -> serde_json::Result { 30 | serde_json::to_string(self) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/stellation/src/guides/migration_01_02.rs: -------------------------------------------------------------------------------- 1 | //! Migration Guide from v0.1 to v0.2 2 | //! 3 | //! # 1. `stellation-backend` crate has been separated into multiple crates. 4 | //! 5 | //! `stellation-backend` has been separated into multiple crates: 6 | //! 7 | //! 1. `stellation-backend`: contains server renderer and other utilities to build backends. 8 | //! 2. `stellation-backend-warp`: contains server that can be used as a warp filter. 9 | //! 3. `stellation-backend-tower`: contains server that can be used as a tower service. 10 | //! 4. `stellation-backend-cli`: contain out-of-the-box command line utility for backend 11 | //! applications. 12 | //! 13 | //! # 2. `stellation-bridge` has been rewritten. 14 | //! 15 | //! Previously, it uses feature flags to switch between local and remote bridges. 16 | //! This has been switched to a link based system. 17 | //! 18 | //! Since this is a complete rewrite, we recommend users to refer to the new [fullstack](https://github.com/futursolo/stellation/tree/main/examples/fullstack) example about 19 | //! how to use the new bridge. 20 | //! 21 | //! # 3. Bounce is updated to version v0.7 22 | -------------------------------------------------------------------------------- /templates/default/crates/server/src/main.rs.liquid: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(missing_debug_implementations)] 3 | 4 | use {{crate_name}}_api as api; 5 | use {{crate_name}}_view as view; 6 | use stellation_backend_tower::TowerEndpoint; 7 | use stellation_backend_cli::Cli; 8 | 9 | mod app; 10 | mod bridge; 11 | use app::ServerApp; 12 | use bridge::create_backend_bridge; 13 | 14 | #[cfg(stellation_embedded_frontend)] 15 | #[derive(rust_embed::RustEmbed)] 16 | #[folder = "$STELLATION_FRONTEND_BUILD_DIR"] 17 | struct Frontend; 18 | 19 | #[tokio::main] 20 | async fn main() -> anyhow::Result<()> { 21 | // Configures Logging 22 | stellation_backend_cli::trace::init_default("STELLATION_APP_SERVER_LOG"); 23 | 24 | // Creates Endpoint 25 | let endpoint = TowerEndpoint::>::new().with_create_bridge(create_backend_bridge); 26 | 27 | #[cfg(stellation_embedded_frontend)] 28 | let endpoint = 29 | endpoint.with_frontend(stellation_backend_tower::Frontend::new_embedded::()); 30 | 31 | // Starts Server 32 | Cli::builder().endpoint(endpoint).build().run().await?; 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/links/mod.rs: -------------------------------------------------------------------------------- 1 | //! The links used to resolve routines. 2 | //! 3 | //! For server-sided links, a new link should be created for each connection. 4 | 5 | use async_trait::async_trait; 6 | 7 | use crate::routines::{BridgedMutation, BridgedQuery, MutationResult, QueryResult}; 8 | use crate::BridgeResult; 9 | mod fetch_link; 10 | mod local_link; 11 | mod phantom_link; 12 | 13 | pub use fetch_link::FetchLink; 14 | pub use local_link::LocalLink; 15 | pub use phantom_link::PhantomLink; 16 | 17 | /// Common methods across all links. 18 | #[async_trait(?Send)] 19 | pub trait Link: PartialEq + Clone { 20 | /// Resolves a Query. 21 | async fn resolve_query(&self, input: &T::Input) -> QueryResult 22 | where 23 | T: 'static + BridgedQuery; 24 | 25 | /// Resolves a Mutation. 26 | async fn resolve_mutation(&self, input: &T::Input) -> MutationResult 27 | where 28 | T: 'static + BridgedMutation; 29 | 30 | /// Resolve a routine with encoded input. 31 | /// 32 | /// Returns `BridgeError` when a malformed input is provided. 33 | async fn resolve_encoded(&self, input_buf: &[u8]) -> BridgeResult>; 34 | } 35 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/links/phantom_link.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use async_trait::async_trait; 4 | 5 | use super::Link; 6 | use crate::routines::{BridgedMutation, BridgedQuery, MutationResult, QueryResult}; 7 | use crate::BridgeResult; 8 | 9 | /// A Link that does nothing. 10 | /// 11 | /// This is used as a type parameter for types that may or may not have a link. 12 | #[derive(Debug, Clone)] 13 | pub struct PhantomLink { 14 | _marker: PhantomData<()>, 15 | } 16 | 17 | impl PartialEq for PhantomLink { 18 | fn eq(&self, _other: &Self) -> bool { 19 | true 20 | } 21 | } 22 | 23 | #[async_trait(?Send)] 24 | impl Link for PhantomLink { 25 | async fn resolve_encoded(&self, _input_buf: &[u8]) -> BridgeResult> { 26 | unimplemented!() 27 | } 28 | 29 | async fn resolve_query(&self, _input: &T::Input) -> QueryResult 30 | where 31 | T: 'static + BridgedQuery, 32 | { 33 | unimplemented!() 34 | } 35 | 36 | async fn resolve_mutation(&self, _input: &T::Input) -> MutationResult 37 | where 38 | T: 'static + BridgedMutation, 39 | { 40 | unimplemented!() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/stellation-backend-tower/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Stellation's tower support. 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_debug_implementations)] 5 | #![deny(unsafe_code)] 6 | #![deny(non_snake_case)] 7 | #![deny(clippy::cognitive_complexity)] 8 | #![deny(missing_docs)] 9 | #![cfg_attr(documenting, feature(doc_cfg))] 10 | #![cfg_attr(documenting, feature(doc_auto_cfg))] 11 | #![cfg_attr(any(releasing, not(debug_assertions)), deny(dead_code, unused_imports))] 12 | 13 | mod endpoint; 14 | pub use endpoint::TowerEndpoint; 15 | /// A stellation request with information extracted with tower services, used for server-side 16 | /// rendering. 17 | /// 18 | /// Currently, this is a type alias to 19 | /// [`WarpRenderRequest`](stellation_backend_warp::WarpRenderRequest). 20 | pub type TowerRenderRequest = stellation_backend_warp::WarpRenderRequest; 21 | /// A stellation request with information extracted with tower services. 22 | /// 23 | /// Currently, this is a type alias to 24 | /// [`WarpRequest`](stellation_backend_warp::WarpRequest). 25 | pub type TowerRequest = stellation_backend_warp::WarpRequest; 26 | #[doc(inline)] 27 | pub use stellation_backend_warp::Frontend; 28 | 29 | mod server; 30 | pub use server::Server; 31 | -------------------------------------------------------------------------------- /crates/stellation-frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellation-frontend" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.66" 6 | repository = "https://github.com/futursolo/stellation" 7 | authors = ["Kaede Hoshiakwa "] 8 | description = "The framework experience for Yew." 9 | keywords = ["web", "wasm", "yew", "framework", "ssr"] 10 | categories = ["wasm", "web-programming"] 11 | readme = "../../README.md" 12 | homepage = "https://github.com/futursolo/stellation" 13 | license = "MIT OR Apache-2.0" 14 | 15 | [dependencies] 16 | yew = { version = "0.20", features = ["csr", "hydration"] } 17 | bounce = { version = "0.8", features = ["helmet"] } 18 | yew-router = "0.17" 19 | tracing = "0.1" 20 | tracing-web = "0.1.2" 21 | tracing-subscriber = { version = "0.3.17", default-features = false, features = [ 22 | "time", 23 | "std", 24 | "fmt", 25 | "ansi", 26 | ] } 27 | anymap2 = "0.13.0" 28 | 29 | # Stellation Components 30 | stellation-bridge = { version = "0.3.0", path = "../stellation-bridge" } 31 | 32 | [dependencies.web-sys] 33 | version = "0.3" 34 | features = ["Document"] 35 | 36 | [package.metadata.docs.rs] 37 | all-features = true 38 | rustdoc-args = ["--cfg", "documenting"] 39 | -------------------------------------------------------------------------------- /crates/stellation-backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellation-backend" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.66" 6 | repository = "https://github.com/futursolo/stellation" 7 | authors = ["Kaede Hoshiakwa "] 8 | description = "The framework experience for Yew." 9 | keywords = ["web", "wasm", "yew", "framework", "ssr"] 10 | categories = ["wasm", "web-programming"] 11 | readme = "../../README.md" 12 | homepage = "https://github.com/futursolo/stellation" 13 | license = "MIT OR Apache-2.0" 14 | 15 | [dependencies] 16 | futures = { version = "0.3", default-features = false, features = ["std"] } 17 | serde = { version = "1", features = ["derive"] } 18 | thiserror = "1" 19 | thread_local = "1.1.7" 20 | lol_html = "1.1.1" 21 | serde_urlencoded = "0.7.1" 22 | anymap2 = "0.13.0" 23 | http = "0.2.9" 24 | 25 | # Stellation Components 26 | stellation-bridge = { version = "0.3.0", path = "../stellation-bridge" } 27 | stellation-core = { version = "0.3.0", path = "../stellation-core" } 28 | 29 | # Yew / Component Related 30 | yew = { version = "0.20", features = ["ssr"] } 31 | bounce = { version = "0.8", features = ["helmet", "ssr"] } 32 | yew-router = "0.17" 33 | 34 | [package.metadata.docs.rs] 35 | all-features = true 36 | rustdoc-args = ["--cfg", "documenting"] 37 | -------------------------------------------------------------------------------- /crates/stellation-backend/src/request.rs: -------------------------------------------------------------------------------- 1 | use http::HeaderMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::ServerAppResult; 5 | 6 | /// A trait that describes a request received by the backend. 7 | pub trait Request { 8 | /// A request context that can be used to provide other information. 9 | type Context; 10 | 11 | /// Returns the path of current request. 12 | fn path(&self) -> &str; 13 | 14 | /// Returns queries as a raw string. 15 | fn raw_queries(&self) -> &str; 16 | 17 | /// Returns the headers of current request. 18 | fn headers(&self) -> &HeaderMap; 19 | 20 | /// Returns queries of current request. 21 | fn queries(&self) -> ServerAppResult 22 | where 23 | Q: Serialize + for<'de> Deserialize<'de>, 24 | { 25 | Ok(serde_urlencoded::from_str(self.raw_queries())?) 26 | } 27 | 28 | /// Returns the current request context. 29 | fn context(&self) -> &Self::Context; 30 | } 31 | 32 | /// A trait that describes a request for server-side rendering. 33 | pub trait RenderRequest: Request { 34 | /// Returns the template of the html file. 35 | fn template(&self) -> &str; 36 | 37 | /// Returns true if this request should be rendered at the client side. 38 | fn is_client_only(&self) -> bool; 39 | } 40 | -------------------------------------------------------------------------------- /crates/stellation-backend-tower/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellation-backend-tower" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.66" 6 | repository = "https://github.com/futursolo/stellation" 7 | authors = ["Kaede Hoshiakwa "] 8 | description = "The framework experience for Yew." 9 | keywords = ["web", "wasm", "yew", "framework", "ssr"] 10 | categories = ["wasm", "web-programming"] 11 | readme = "../../README.md" 12 | homepage = "https://github.com/futursolo/stellation" 13 | license = "MIT OR Apache-2.0" 14 | 15 | [dependencies] 16 | hyper = { version = "0.14.27", features = ["runtime", "server", "http1"] } 17 | tower = { version = "0.4", features = ["util"] } 18 | tokio = { version = "1" } 19 | futures = { version = "0.3", default-features = false, features = ["std"] } 20 | yew = { version = "0.20", features = ["ssr"] } 21 | warp = { version = "0.3.5", default-features = false } 22 | 23 | # Stellation Components 24 | stellation-backend-warp = { version = "0.3.0", path = "../stellation-backend-warp" } 25 | stellation-backend = { version = "0.3.0", path = "../stellation-backend" } 26 | stellation-bridge = { version = "0.3.0", path = "../stellation-bridge" } 27 | 28 | [package.metadata.docs.rs] 29 | all-features = true 30 | rustdoc-args = ["--cfg", "documenting"] 31 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/resolvers.rs: -------------------------------------------------------------------------------- 1 | //! Bridge resolvers. 2 | 3 | use async_trait::async_trait; 4 | 5 | use crate::routines::{BridgedMutation, BridgedQuery, MutationResult, QueryResult}; 6 | 7 | /// The resolver of a bridge query. 8 | /// 9 | /// This type is required to be implemented for `LocalLink`. 10 | /// Please refer to the crate implementation for more information. 11 | #[async_trait(?Send)] 12 | pub trait QueryResolver: BridgedQuery { 13 | /// The context type. 14 | /// 15 | /// This type needs to match the `CTX` type parameter of the bridge it is added. 16 | type Context: 'static; 17 | 18 | /// Resolves the current query. 19 | async fn resolve(meta: &Self::Context, input: &Self::Input) -> QueryResult; 20 | } 21 | 22 | /// The resolver of a bridge mutation. 23 | /// 24 | /// This type is required to be implemented for `LocalLink`. 25 | /// Please refer to the crate implementation for more information. 26 | #[async_trait(?Send)] 27 | pub trait MutationResolver: BridgedMutation { 28 | /// The context type. 29 | /// 30 | /// This type needs to match the `CTX` type parameter of the bridge it is added. 31 | type Context: 'static; 32 | 33 | /// Resolves the current mutation. 34 | async fn resolve(meta: &Self::Context, input: &Self::Input) -> MutationResult; 35 | } 36 | -------------------------------------------------------------------------------- /templates/default/crates/client/Cargo.toml.liquid: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{project-name}}-client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | yew = "0.20.0" 11 | gloo = "0.8" 12 | 13 | {% if stellation_target == "release" %} 14 | # Stellation 15 | stellation-frontend = "{{stellation_release_ver}}" 16 | stellation-stylist = { version = "{{stellation_release_ver}}", features = ["frontend"] } 17 | 18 | {% elsif stellation_target == "main" %} 19 | # Stellation 20 | stellation-frontend = { git = "https://github.com/futursolo/stellation" } 21 | stellation-stylist = { git = "https://github.com/futursolo/stellation", features = ["frontend"] } 22 | 23 | {% elsif stellation_target == "ci" %} 24 | # Stellation 25 | stellation-frontend = { path = "../../../../stellation/crates/stellation-frontend" } 26 | stellation-stylist = { path = "../../../../stellation/crates/stellation-stylist", features = ["frontend"] } 27 | 28 | {% endif %} 29 | # Logging 30 | tracing = "0.1" 31 | tracing-subscriber = { version = "0.3.16", default-features = false, features = ["time", "std", "fmt", "ansi"] } 32 | 33 | # Example Workspace 34 | {{project-name}}-view = { path = "../view" } 35 | {{project-name}}-api = { path = "../api" } 36 | 37 | -------------------------------------------------------------------------------- /templates/default/crates/view/Cargo.toml.liquid: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{project-name}}-view" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | yew = { version = "0.20.0" } 11 | {{project-name}}-api = { path = "../api" } 12 | time = { version = "0.3", features = ["wasm-bindgen", "serde-human-readable", "macros"] } 13 | tracing = "0.1.37" 14 | bounce = { version = "0.8.0", features = ["helmet"] } 15 | stylist = { version = "0.12.0", features = ["yew_integration"] } 16 | 17 | {% if stellation_target == "release" %} 18 | # Stellation 19 | stellation-frontend = "{{stellation_release_ver}}" 20 | stellation-bridge = "{{stellation_release_ver}}" 21 | 22 | {% elsif stellation_target == "main" %} 23 | # Stellation 24 | stellation-frontend = { git = "https://github.com/futursolo/stellation" } 25 | stellation-bridge = { git = "https://github.com/futursolo/stellation" } 26 | 27 | {% elsif stellation_target == "ci" %} 28 | # Stellation 29 | stellation-frontend = { path = "../../../../stellation/crates/stellation-frontend" } 30 | stellation-bridge = { path = "../../../../stellation/crates/stellation-bridge" } 31 | 32 | {% endif %} 33 | [dependencies.web-sys] 34 | version = "0.3" 35 | features = [ 36 | "HtmlInputElement" 37 | ] 38 | -------------------------------------------------------------------------------- /examples/fullstack/api/src/resolvers.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use stellation_bridge::links::LocalLink; 3 | use stellation_bridge::registry::ResolverRegistry; 4 | use stellation_bridge::resolvers::{MutationResolver, QueryResolver}; 5 | use stellation_bridge::routines::{MutationResult, QueryResult}; 6 | use stellation_bridge::Bridge as Bridge_; 7 | use time::OffsetDateTime; 8 | 9 | pub use crate::routines::*; 10 | 11 | #[async_trait(?Send)] 12 | impl QueryResolver for ServerTimeQuery { 13 | type Context = (); 14 | 15 | async fn resolve(_ctx: &(), _input: &Self::Input) -> QueryResult { 16 | Ok(Self { 17 | value: OffsetDateTime::now_utc(), 18 | } 19 | .into()) 20 | } 21 | } 22 | 23 | #[async_trait(?Send)] 24 | impl MutationResolver for GreetingMutation { 25 | type Context = (); 26 | 27 | async fn resolve(_ctx: &(), name: &Self::Input) -> MutationResult { 28 | Ok(Self { 29 | message: format!("Hello, {name}!"), 30 | } 31 | .into()) 32 | } 33 | } 34 | 35 | pub fn create_resolver_registry() -> ResolverRegistry<()> { 36 | ResolverRegistry::<()>::builder() 37 | .add_query::() 38 | .add_mutation::() 39 | .build() 40 | } 41 | 42 | pub type Link = LocalLink<()>; 43 | pub type Bridge = Bridge_; 44 | -------------------------------------------------------------------------------- /templates/default/crates/api/src/resolvers.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use stellation_bridge::links::LocalLink; 3 | use stellation_bridge::registry::ResolverRegistry; 4 | use stellation_bridge::resolvers::{MutationResolver, QueryResolver}; 5 | use stellation_bridge::routines::{MutationResult, QueryResult}; 6 | use stellation_bridge::Bridge as Bridge_; 7 | use time::OffsetDateTime; 8 | 9 | pub use crate::routines::*; 10 | 11 | #[async_trait(?Send)] 12 | impl QueryResolver for ServerTimeQuery { 13 | type Context = (); 14 | 15 | async fn resolve(_ctx: &(), _input: &Self::Input) -> QueryResult { 16 | Ok(Self { 17 | value: OffsetDateTime::now_utc(), 18 | } 19 | .into()) 20 | } 21 | } 22 | 23 | #[async_trait(?Send)] 24 | impl MutationResolver for GreetingMutation { 25 | type Context = (); 26 | 27 | async fn resolve(_ctx: &(), name: &Self::Input) -> MutationResult { 28 | Ok(Self { 29 | message: format!("Hello, {name}!"), 30 | } 31 | .into()) 32 | } 33 | } 34 | 35 | pub fn create_resolver_registry() -> ResolverRegistry<()> { 36 | ResolverRegistry::<()>::builder() 37 | .add_query::() 38 | .add_mutation::() 39 | .build() 40 | } 41 | 42 | pub type Link = LocalLink<()>; 43 | pub type Bridge = Bridge_; 44 | -------------------------------------------------------------------------------- /examples/fullstack/view/src/pages/server_time.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use example_fullstack_api::{Bridge, ServerTimeQuery}; 4 | use time::macros::format_description; 5 | use yew::platform::spawn_local; 6 | use yew::platform::time::sleep; 7 | use yew::prelude::*; 8 | 9 | #[function_component] 10 | pub fn ServerTime() -> HtmlResult { 11 | let server_time = Bridge::use_query::(().into())?; 12 | { 13 | let server_time = server_time.clone(); 14 | 15 | use_effect_with_deps( 16 | move |_| { 17 | spawn_local(async move { 18 | loop { 19 | sleep(Duration::from_secs(1)).await; 20 | let _ = server_time.refresh().await; 21 | } 22 | }); 23 | }, 24 | (), 25 | ); 26 | } 27 | 28 | let server_time = match server_time.as_deref() { 29 | Ok(m) => m 30 | .value 31 | .format(format_description!( 32 | "[year]-[month]-[day] [hour]:[minute]:[second]" 33 | )) 34 | .expect("failed to format time!"), 35 | Err(_) => { 36 | return Ok(html! { 37 |
{"Waiting for Server..."}
38 | }) 39 | } 40 | }; 41 | 42 | Ok(html! { 43 |
{"Server Time: "}{server_time}
44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /templates/default/crates/view/src/pages/server_time.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use time::macros::format_description; 4 | use yew::platform::spawn_local; 5 | use yew::platform::time::sleep; 6 | use yew::prelude::*; 7 | 8 | use crate::api::{Bridge, ServerTimeQuery}; 9 | 10 | #[function_component] 11 | pub fn ServerTime() -> HtmlResult { 12 | let server_time = Bridge::use_query::(().into())?; 13 | { 14 | let server_time = server_time.clone(); 15 | 16 | use_effect_with_deps( 17 | move |_| { 18 | spawn_local(async move { 19 | loop { 20 | sleep(Duration::from_secs(1)).await; 21 | let _ = server_time.refresh().await; 22 | } 23 | }); 24 | }, 25 | (), 26 | ); 27 | } 28 | 29 | let server_time = match server_time.as_deref() { 30 | Ok(m) => m 31 | .value 32 | .format(format_description!( 33 | "[year]-[month]-[day] [hour]:[minute]:[second]" 34 | )) 35 | .expect("failed to format time!"), 36 | Err(_) => { 37 | return Ok(html! { 38 |
{"Waiting for Server..."}
39 | }) 40 | } 41 | }; 42 | 43 | Ok(html! { 44 |
{"Server Time: "}{server_time}
45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /crates/stellation-backend-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellation-backend-cli" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.66" 6 | repository = "https://github.com/futursolo/stellation" 7 | authors = ["Kaede Hoshiakwa "] 8 | description = "The framework experience for Yew." 9 | keywords = ["web", "wasm", "yew", "framework", "ssr"] 10 | categories = ["wasm", "web-programming"] 11 | readme = "../../README.md" 12 | homepage = "https://github.com/futursolo/stellation" 13 | license = "MIT OR Apache-2.0" 14 | 15 | [dependencies] 16 | # Stellation Components 17 | stellation-backend = { version = "0.3.0", path = "../stellation-backend" } 18 | stellation-backend-tower = { version = "0.3.0", path = "../stellation-backend-tower" } 19 | stellation-core = { version = "0.3.0", path = "../stellation-core" } 20 | stellation-bridge = { version = "0.3.0", path = "../stellation-bridge" } 21 | 22 | # Yew / Component Related 23 | yew = { version = "0.20", features = ["ssr"] } 24 | 25 | # Other Deps 26 | anyhow = { version = "1" } 27 | clap = { version = "4.4.2", features = ["derive", "env"] } 28 | tracing = { version = "0.1.37" } 29 | tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } 30 | console = "0.15.7" 31 | typed-builder = "0.16.0" 32 | tower = "0.4.13" 33 | hyper = "0.14.27" 34 | 35 | [package.metadata.docs.rs] 36 | all-features = true 37 | rustdoc-args = ["--cfg", "documenting"] 38 | -------------------------------------------------------------------------------- /crates/stellation-backend-warp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stellation-backend-warp" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.66" 6 | repository = "https://github.com/futursolo/stellation" 7 | authors = ["Kaede Hoshiakwa "] 8 | description = "The framework experience for Yew." 9 | keywords = ["web", "wasm", "yew", "framework", "ssr"] 10 | categories = ["wasm", "web-programming"] 11 | readme = "../../README.md" 12 | homepage = "https://github.com/futursolo/stellation" 13 | license = "MIT OR Apache-2.0" 14 | 15 | [dependencies] 16 | # Yew / Component Related 17 | yew = { version = "0.20", features = ["ssr"] } 18 | yew-router = "0.17" 19 | bounce = { version = "0.8", features = ["helmet", "ssr"] } 20 | 21 | # Stellation Components 22 | stellation-backend = { version = "0.3.0", path = "../stellation-backend" } 23 | stellation-bridge = { version = "0.3.0", path = "../stellation-bridge" } 24 | 25 | # HTTP 26 | hyper = { version = "0.14.27", features = ["runtime", "server", "http1"] } 27 | warp = { version = "0.3.5", default-features = false, features = ["websocket"] } 28 | serde_urlencoded = "0.7.1" 29 | bytes = { version = "1" } 30 | http = { version = "0.2" } 31 | rust-embed = { version = "8.0.0" } 32 | mime_guess = "2.0.4" 33 | lol_html = "1.1.1" 34 | 35 | # Other 36 | futures = { version = "0.3", default-features = false, features = ["std"] } 37 | tokio = { version = "1" } 38 | once_cell = "1.18.0" 39 | tracing = { version = "0.1.37" } 40 | rand = "0.8.5" 41 | 42 | [package.metadata.docs.rs] 43 | all-features = true 44 | rustdoc-args = ["--cfg", "documenting"] 45 | -------------------------------------------------------------------------------- /crates/stctl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stctl" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.66" 6 | repository = "https://github.com/futursolo/stellation" 7 | authors = ["Kaede Hoshiakwa "] 8 | description = "The framework experience for Yew." 9 | keywords = ["web", "wasm", "yew", "framework", "ssr"] 10 | categories = ["wasm", "web-programming"] 11 | readme = "../../README.md" 12 | homepage = "https://github.com/futursolo/stellation" 13 | license = "MIT OR Apache-2.0" 14 | 15 | [dependencies] 16 | anyhow = "1.0.75" 17 | clap = { version = "4.4.2", features = ["derive"] } 18 | serde = { version = "1.0.188", features = ["derive"] } 19 | tokio = { version = "1.32.0", features = ["full"] } 20 | toml = "0.7.6" 21 | tracing = { version = "0.1.37" } 22 | tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } 23 | notify = "6.1.1" 24 | futures = { version = "0.3", features = [ 25 | "std", 26 | "async-await", 27 | ], default-features = false } 28 | cargo_metadata = "0.17" 29 | serde_json = "1.0.105" 30 | dotenvy = "0.15.7" 31 | time = "0.3" 32 | rand = "0.8.5" 33 | indicatif = "0.17.6" 34 | console = "0.15.7" 35 | reqwest = { version = "0.11.20", features = [ 36 | "rustls-tls-webpki-roots", 37 | "stream", 38 | ], default-features = false } 39 | tokio-stream = { version = "0.1.14", features = ["fs", "sync"] } 40 | webbrowser = "0.8.11" 41 | 42 | # Stellation Components 43 | stellation-core = { version = "0.3.0", path = "../stellation-core" } 44 | fs_extra = "1.3.0" 45 | 46 | [package.metadata.docs.rs] 47 | all-features = true 48 | rustdoc-args = ["--cfg", "documenting"] 49 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Bridge between the frontend and backend. 2 | //! 3 | //! This module is a RPC implementation that facilitates communications between frontend and 4 | //! backend. 5 | //! 6 | //! It supports the following routines: 7 | //! 8 | //! - [Query](routines::BridgedQuery) 9 | //! - [Mutation](routines::BridgedMutation) 10 | //! 11 | //! Bridge has 2 connection methods `local` and `remote`. When a `LocalLink` is used, routines will 12 | //! be connected with the local method and can process requests with resolvers. This can be used for 13 | //! server-side rendering and processing requests from a bridge connected with the remote method. If 14 | //! the `FetchLink` is used, it will send the request to the bridge endpoint which will 15 | //! process the routine at the server-side. This is usually used for client-side rendering. 16 | //! 17 | //! You can check out the [example](https://github.com/futursolo/stellation/blob/main/examples/fullstack/api/src/lib.rs) for how to implement resolvers. 18 | 19 | #![deny(clippy::all)] 20 | #![deny(missing_debug_implementations)] 21 | #![deny(unsafe_code)] 22 | #![deny(non_snake_case)] 23 | #![deny(clippy::cognitive_complexity)] 24 | #![deny(missing_docs)] 25 | #![cfg_attr(documenting, feature(doc_cfg))] 26 | #![cfg_attr(documenting, feature(doc_auto_cfg))] 27 | #![cfg_attr(any(releasing, not(debug_assertions)), deny(dead_code, unused_imports))] 28 | 29 | mod bridge; 30 | mod error; 31 | pub mod hooks; 32 | pub mod links; 33 | pub mod registry; 34 | pub mod resolvers; 35 | pub mod routines; 36 | pub mod state; 37 | 38 | pub use bridge::Bridge; 39 | pub use error::{BridgeError, BridgeResult}; 40 | -------------------------------------------------------------------------------- /crates/stctl/src/indicators.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use indicatif::{ProgressBar, ProgressStyle}; 4 | 5 | fn create_progress(total_steps: u64) -> ProgressBar { 6 | let bar = ProgressBar::new(total_steps); 7 | // Progress Bar needs to be updated in a different thread. 8 | { 9 | let bar = bar.downgrade(); 10 | std::thread::spawn(move || { 11 | while let Some(bar) = bar.upgrade() { 12 | bar.tick(); 13 | std::thread::sleep(Duration::from_millis(100)); 14 | } 15 | }); 16 | } 17 | 18 | bar.set_style( 19 | ProgressStyle::default_bar() 20 | .template("{spinner:.green} {prefix} [{elapsed_precise}] [{bar:20}]") 21 | .expect("failed to parse template") 22 | // .tick_chars("-\\|/") 23 | .progress_chars("=>-"), 24 | ); 25 | 26 | bar 27 | } 28 | 29 | pub(crate) struct ServeProgress { 30 | inner: ProgressBar, 31 | } 32 | 33 | impl ServeProgress { 34 | pub fn new() -> Self { 35 | Self { 36 | inner: create_progress(20), 37 | } 38 | } 39 | 40 | pub fn step_build_frontend(&self) { 41 | self.inner.set_prefix("Building (frontend) "); 42 | self.inner.set_position(2); 43 | } 44 | 45 | pub fn step_build_backend(&self) { 46 | self.inner.set_prefix("Building (backend) "); 47 | self.inner.set_position(10); 48 | } 49 | 50 | pub fn step_starting(&self) { 51 | self.inner.set_prefix("Starting "); 52 | self.inner.set_position(17); 53 | } 54 | 55 | pub fn hide(self) { 56 | self.inner.finish_and_clear() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/stctl/src/env_file.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::HashMap; 3 | use std::env; 4 | use std::path::Path; 5 | 6 | #[derive(Debug, Clone)] 7 | pub(crate) struct EnvFile { 8 | name: String, 9 | } 10 | 11 | impl EnvFile { 12 | pub fn new(name: S) -> Self 13 | where 14 | S: Into, 15 | { 16 | Self { name: name.into() } 17 | } 18 | 19 | pub fn load

(&self, workspace_dir: P) -> HashMap 20 | where 21 | P: AsRef, 22 | { 23 | let workspace_dir = workspace_dir.as_ref(); 24 | let mut envs = HashMap::new(); 25 | 26 | for env_name in [ 27 | Cow::from(".env"), 28 | ".env.local".into(), 29 | format!(".env.{}", self.name).into(), 30 | format!(".env.{}.local", self.name).into(), 31 | ] { 32 | let path = workspace_dir.join(env_name.as_ref()); 33 | if path.exists() { 34 | if let Err(e) = dotenvy::from_path_iter(&path).and_then(|m| { 35 | for i in m { 36 | let (k, v) = i?; 37 | if env::var(&k).is_ok() { 38 | // environment variables inherited from current process have a higher 39 | // priority. 40 | continue; 41 | } 42 | 43 | envs.insert(k, v); 44 | } 45 | 46 | Ok(()) 47 | }) { 48 | tracing::warn!(path = %path.display(), reason = ?e, "failed to load environment file"); 49 | } 50 | } 51 | } 52 | 53 | envs 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ci/update-version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import tomlkit 4 | import glob 5 | import sys 6 | from pathlib import Path 7 | 8 | def main() -> None: 9 | cwd = Path.cwd() 10 | next_ver = sys.argv[1] 11 | 12 | if next_ver.startswith('v'): 13 | next_ver = next_ver[1:] 14 | 15 | print(f"Running in {cwd}...") 16 | 17 | for cargo_toml_path in cwd.glob("crates/*/Cargo.toml"): 18 | cfg = tomlkit.loads(cargo_toml_path.open().read()) 19 | print(f"Updating {cargo_toml_path} to version {next_ver}...") 20 | 21 | cfg["package"]["version"] = next_ver 22 | 23 | for (key, value) in cfg["dependencies"].items(): 24 | if not isinstance(value, dict) or "path" not in value.keys(): 25 | print(f" Skipping {key}...") 26 | continue 27 | 28 | print(f" Updating {key} to version {next_ver}...") 29 | value["version"] = next_ver 30 | 31 | with cargo_toml_path.open("w") as f: 32 | f.write(tomlkit.dumps(cfg)) 33 | f.flush() 34 | 35 | for cargo_toml_path in cwd.glob("examples/**/Cargo.toml"): 36 | cfg = tomlkit.loads(cargo_toml_path.open().read()) 37 | print(f"Updating example {cargo_toml_path}...") 38 | 39 | for (key, value) in cfg["dependencies"].items(): 40 | if not isinstance(value, dict) or "path" not in value.keys() or "version" not in value.keys(): 41 | print(f" Skipping {key}...") 42 | continue 43 | 44 | print(f" Updating {key} to version {next_ver}...") 45 | value["version"] = next_ver 46 | 47 | with cargo_toml_path.open("w") as f: 48 | f.write(tomlkit.dumps(cfg)) 49 | f.flush() 50 | 51 | if __name__ == '__main__': 52 | main() 53 | -------------------------------------------------------------------------------- /crates/stellation-backend/src/utils/thread_local.rs: -------------------------------------------------------------------------------- 1 | //! A type to clone fn once per thread. 2 | 3 | use std::fmt; 4 | use std::ops::Deref; 5 | use std::sync::{Arc, Mutex}; 6 | 7 | use thread_local::ThreadLocal; 8 | 9 | /// A value that is lazily initialised once per thread. 10 | pub struct ThreadLocalLazy { 11 | value: Arc>, 12 | create_value: Arc T>, 13 | } 14 | 15 | impl fmt::Debug for ThreadLocalLazy { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | f.write_str("ThreadLocalLazy<_>") 18 | } 19 | } 20 | 21 | impl Clone for ThreadLocalLazy 22 | where 23 | T: 'static + Send, 24 | { 25 | fn clone(&self) -> Self { 26 | Self { 27 | value: self.value.clone(), 28 | create_value: self.create_value.clone(), 29 | } 30 | } 31 | } 32 | 33 | impl ThreadLocalLazy 34 | where 35 | T: 'static + Send, 36 | { 37 | /// Creates a thread-local lazy value. 38 | /// 39 | /// The create function is called once per thread. 40 | pub fn new(f: F) -> Self 41 | where 42 | F: 'static + Send + Fn() -> T, 43 | { 44 | let clonable_inner = Arc::new(Mutex::new(f)); 45 | let create_inner = move || -> T { 46 | let clonable_inner = clonable_inner.lock().expect("failed to lock?"); 47 | clonable_inner() 48 | }; 49 | 50 | Self { 51 | value: Arc::new(ThreadLocal::new()), 52 | create_value: Arc::new(create_inner), 53 | } 54 | } 55 | } 56 | 57 | impl Deref for ThreadLocalLazy 58 | where 59 | T: 'static + Send, 60 | { 61 | type Target = T; 62 | 63 | fn deref(&self) -> &Self::Target { 64 | self.value.get_or(&*self.create_value) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/stellation-frontend/src/root.rs: -------------------------------------------------------------------------------- 1 | use anymap2::AnyMap; 2 | use bounce::helmet::HelmetBridge; 3 | use bounce::BounceRoot; 4 | use stellation_bridge::links::Link; 5 | use stellation_bridge::state::BridgeState; 6 | use yew::prelude::*; 7 | use yew_router::BrowserRouter; 8 | 9 | #[derive(Properties)] 10 | pub(crate) struct StellationRootProps 11 | where 12 | L: Link, 13 | { 14 | #[prop_or_default] 15 | pub children: Html, 16 | pub bridge_state: Option>, 17 | } 18 | 19 | impl PartialEq for StellationRootProps 20 | where 21 | L: Link, 22 | { 23 | fn eq(&self, other: &Self) -> bool { 24 | self.children == other.children && self.bridge_state == other.bridge_state 25 | } 26 | } 27 | 28 | impl Clone for StellationRootProps 29 | where 30 | L: Link, 31 | { 32 | fn clone(&self) -> Self { 33 | Self { 34 | children: self.children.clone(), 35 | bridge_state: self.bridge_state.clone(), 36 | } 37 | } 38 | } 39 | 40 | #[function_component] 41 | pub(crate) fn StellationRoot(props: &StellationRootProps) -> Html 42 | where 43 | L: 'static + Link, 44 | { 45 | let StellationRootProps { 46 | children, 47 | bridge_state, 48 | } = props.clone(); 49 | 50 | let get_init_states = use_callback( 51 | move |_, bridge_state| { 52 | let mut states = AnyMap::new(); 53 | 54 | if let Some(m) = bridge_state.clone() { 55 | states.insert(m); 56 | } 57 | 58 | states 59 | }, 60 | bridge_state, 61 | ); 62 | 63 | html! { 64 | 65 | 66 | 67 | {children} 68 | 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/fullstack/view/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(missing_debug_implementations)] 3 | 4 | use bounce::helmet::Helmet; 5 | use stylist::yew::{styled_component, Global}; 6 | use yew::prelude::*; 7 | 8 | mod pages; 9 | use pages::{Greeting, ServerTime}; 10 | 11 | #[styled_component] 12 | pub fn Main() -> Html { 13 | let fallback = html! {

{"Loading..."}
}; 14 | 15 | html! { 16 | <> 17 | 34 | 35 | {"Welcome to Stellation!"} 36 | 37 |
46 |
50 | {"Welcome to Stellation!"} 51 |
52 | 53 | 54 | 55 | 56 |
57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /templates/default/crates/server/Cargo.toml.liquid: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{project-name}}-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | anyhow = "1" 11 | tokio = { version = "1.23.0", features = ["full"] } 12 | tracing = { version = "0.1.37" } 13 | yew = "0.20.0" 14 | rust-embed = { version = "8.0.0", features = ["interpolate-folder-path"] } 15 | 16 | {% if stellation_target == "release" %} 17 | # Stellation 18 | stellation-backend = { version = "{{stellation_release_ver}}" } 19 | stellation-backend-tower = { version = "{{stellation_release_ver}}" } 20 | stellation-backend-cli = { version = "{{stellation_release_ver}}" } 21 | stellation-stylist = { version = "{{stellation_release_ver}}", features = ["backend"] } 22 | 23 | {% elsif stellation_target == "main" %} 24 | # Stellation 25 | stellation-backend = { git = "https://github.com/futursolo/stellation" } 26 | stellation-backend-tower = { git = "https://github.com/futursolo/stellation" } 27 | stellation-backend-cli = { git = "https://github.com/futursolo/stellation" } 28 | stellation-stylist = { git = "https://github.com/futursolo/stellation", features = ["backend"] } 29 | 30 | {% elsif stellation_target == "ci" %} 31 | # Stellation 32 | stellation-backend = { path = "../../../../stellation/crates/stellation-backend" } 33 | stellation-backend-tower = { path = "../../../../stellation/crates/stellation-backend-tower" } 34 | stellation-backend-cli = { path = "../../../../stellation/crates/stellation-backend-cli" } 35 | stellation-stylist = { path = "../../../../stellation/crates/stellation-stylist", features = ["backend"] } 36 | 37 | {% endif %} 38 | # Example Workspace 39 | {{project-name}}-view = { path = "../view" } 40 | {{project-name}}-api = { path = "../api", features = ["resolvable"] } 41 | -------------------------------------------------------------------------------- /templates/default/crates/view/src/lib.rs.liquid: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(missing_debug_implementations)] 3 | 4 | use bounce::helmet::Helmet; 5 | use yew::prelude::*; 6 | use stylist::yew::{styled_component, Global}; 7 | use {{crate_name}}_api as api; 8 | 9 | mod pages; 10 | use pages::{Greeting, ServerTime}; 11 | 12 | #[styled_component] 13 | pub fn Main() -> Html { 14 | let fallback = html! {
{"Loading..."}
}; 15 | 16 | html! { 17 | <> 18 | 35 | 36 | {"Welcome to Stellation!"} 37 | 38 |
47 |
51 | {"Welcome to Stellation!"} 52 |
53 | 54 | 55 | 56 | 57 |
58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/stellation-backend/src/props.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | use std::rc::Rc; 3 | 4 | use http::HeaderMap; 5 | use serde::{Deserialize, Serialize}; 6 | use yew::Properties; 7 | 8 | use crate::error::ServerAppResult; 9 | use crate::Request; 10 | 11 | /// The Properties provided to a server app. 12 | #[derive(Properties, Debug)] 13 | pub struct ServerAppProps { 14 | request: Rc, 15 | _marker: PhantomData, 16 | } 17 | 18 | impl ServerAppProps 19 | where 20 | REQ: Request, 21 | { 22 | /// Returns the path of current request. 23 | pub fn path(&self) -> &str { 24 | self.request.path() 25 | } 26 | 27 | /// Returns queries of current request. 28 | pub fn queries(&self) -> ServerAppResult 29 | where 30 | Q: Serialize + for<'de> Deserialize<'de>, 31 | { 32 | self.request.queries() 33 | } 34 | 35 | /// Returns queries as a raw string. 36 | pub fn raw_queries(&self) -> &str { 37 | self.request.raw_queries() 38 | } 39 | 40 | /// Returns request headers. 41 | pub fn headers(&self) -> &HeaderMap { 42 | self.request.headers() 43 | } 44 | 45 | /// Returns the current request context. 46 | pub fn context(&self) -> &CTX { 47 | self.request.context() 48 | } 49 | 50 | pub(crate) fn from_request(request: Rc) -> Self { 51 | Self { 52 | request, 53 | _marker: PhantomData, 54 | } 55 | } 56 | } 57 | 58 | impl PartialEq for ServerAppProps { 59 | fn eq(&self, other: &Self) -> bool { 60 | Rc::ptr_eq(&self.request, &other.request) 61 | } 62 | } 63 | 64 | impl Clone for ServerAppProps { 65 | fn clone(&self) -> Self { 66 | Self { 67 | request: self.request.clone(), 68 | _marker: PhantomData, 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/stellation-stylist/src/backend.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use stellation_backend::hooks::use_append_head_content; 5 | use stylist::manager::{render_static, StyleManager}; 6 | use stylist::yew::ManagerProvider; 7 | use yew::html::ChildrenProps; 8 | use yew::prelude::*; 9 | 10 | /// A Stylist [`ManagerProvider`] that writes server-side styles to 11 | /// [`ServerRenderer`](stellation_backend::ServerRenderer) automatically. 12 | /// 13 | /// # Panics 14 | /// 15 | /// This provider should be used in the server app instance. Using this component in the client app 16 | /// will panic. 17 | /// 18 | /// This provider requires a [`FrontendManagerProvider`](crate::FrontendManagerProvider) to be 19 | /// placed in the client app or hydration will fail. 20 | /// 21 | /// You can check out this [example](https://github.com/futursolo/stellation/blob/main/examples/fullstack/server/src/app.rs) for how to use this provider. 22 | #[function_component] 23 | pub fn BackendManagerProvider(props: &ChildrenProps) -> Html { 24 | let (reader, manager) = use_memo( 25 | |_| { 26 | let (writer, reader) = render_static(); 27 | 28 | let style_mgr = StyleManager::builder() 29 | .writer(writer) 30 | .build() 31 | .expect("failed to create style manager."); 32 | 33 | (Rc::new(RefCell::new(Some(reader))), style_mgr) 34 | }, 35 | (), 36 | ) 37 | .as_ref() 38 | .to_owned(); 39 | 40 | use_append_head_content(move || async move { 41 | let style_data = reader 42 | .borrow_mut() 43 | .take() 44 | .expect("reader is called twice!") 45 | .read_style_data(); 46 | 47 | let mut s = String::new(); 48 | // Write to a String can never fail. 49 | let _ = style_data.write_static_markup(&mut s); 50 | 51 | s 52 | }); 53 | 54 | html! { 55 | 56 | {props.children.clone()} 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/stellation-backend-warp/src/html.rs: -------------------------------------------------------------------------------- 1 | use lol_html::{doc_comments, rewrite_str, Settings}; 2 | use once_cell::sync::Lazy; 3 | 4 | use crate::SERVER_ID; 5 | 6 | static AUTO_REFRESH_SCRIPT: Lazy = Lazy::new(|| { 7 | format!( 8 | r#" 9 | "#, 42 | SERVER_ID.as_str() 43 | ) 44 | }); 45 | 46 | pub(crate) fn add_refresh_script(html_s: &str) -> String { 47 | rewrite_str( 48 | html_s, 49 | Settings { 50 | document_content_handlers: vec![doc_comments!(|c| { 51 | if c.text() == "%STELLATION_BODY%" { 52 | c.after( 53 | AUTO_REFRESH_SCRIPT.as_str(), 54 | lol_html::html_content::ContentType::Html, 55 | ); 56 | } 57 | Ok(()) 58 | })], 59 | ..Default::default() 60 | }, 61 | ) 62 | .expect("failed to render html") 63 | } 64 | -------------------------------------------------------------------------------- /crates/stellation-backend/src/html.rs: -------------------------------------------------------------------------------- 1 | use bounce::helmet::HelmetTag; 2 | use lol_html::{doc_comments, element, rewrite_str, Settings}; 3 | 4 | pub(crate) async fn format_html(html_s: &str, tags: I, head_s: H, body_s: B) -> String 5 | where 6 | I: IntoIterator, 7 | H: Into, 8 | B: AsRef, 9 | { 10 | let mut head_s = head_s.into(); 11 | let body_s = body_s.as_ref(); 12 | 13 | let mut html_tag = None; 14 | let mut body_tag = None; 15 | 16 | for tag in tags.into_iter() { 17 | match tag { 18 | HelmetTag::Html { .. } => { 19 | html_tag = Some(tag); 20 | } 21 | HelmetTag::Body { .. } => { 22 | body_tag = Some(tag); 23 | } 24 | _ => { 25 | let _ = tag.write_static(&mut head_s); 26 | } 27 | } 28 | } 29 | 30 | rewrite_str( 31 | html_s, 32 | Settings { 33 | element_content_handlers: vec![ 34 | element!("html", |h| { 35 | if let Some(HelmetTag::Html { attrs }) = html_tag.take() { 36 | for (k, v) in attrs { 37 | h.set_attribute(k.as_ref(), v.as_ref())?; 38 | } 39 | } 40 | 41 | Ok(()) 42 | }), 43 | element!("body", |h| { 44 | if let Some(HelmetTag::Body { attrs }) = body_tag.take() { 45 | for (k, v) in attrs { 46 | h.set_attribute(k.as_ref(), v.as_ref())?; 47 | } 48 | } 49 | 50 | Ok(()) 51 | }), 52 | ], 53 | 54 | document_content_handlers: vec![doc_comments!(|c| { 55 | if c.text() == "%STELLATION_HEAD%" { 56 | c.replace(&head_s, lol_html::html_content::ContentType::Html); 57 | } 58 | if c.text() == "%STELLATION_BODY%" { 59 | c.replace(body_s, lol_html::html_content::ContentType::Html); 60 | } 61 | 62 | Ok(()) 63 | })], 64 | ..Default::default() 65 | }, 66 | ) 67 | .expect("failed to render html") 68 | } 69 | -------------------------------------------------------------------------------- /examples/fullstack/api/src/routines.rs: -------------------------------------------------------------------------------- 1 | use bounce::{Atom, Selector}; 2 | use serde::{Deserialize, Serialize}; 3 | use stellation_bridge::links::FetchLink; 4 | use stellation_bridge::registry::RoutineRegistry; 5 | use stellation_bridge::routines::{BridgedMutation, BridgedQuery}; 6 | use stellation_bridge::Bridge as Bridge_; 7 | use thiserror::Error; 8 | use time::OffsetDateTime; 9 | 10 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 11 | pub struct ServerTimeQuery { 12 | pub value: OffsetDateTime, 13 | } 14 | 15 | #[derive(Debug, Error, PartialEq, Eq, Clone, Serialize, Deserialize)] 16 | pub enum Error { 17 | #[error("failed to communicate with server.")] 18 | Network, 19 | } 20 | 21 | impl BridgedQuery for ServerTimeQuery { 22 | type Error = Error; 23 | type Input = (); 24 | 25 | fn into_query_error(_e: stellation_bridge::BridgeError) -> Self::Error { 26 | Error::Network 27 | } 28 | } 29 | 30 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 31 | pub struct GreetingMutation { 32 | pub message: String, 33 | } 34 | 35 | impl BridgedMutation for GreetingMutation { 36 | type Error = Error; 37 | type Input = String; 38 | 39 | fn into_mutation_error(_e: stellation_bridge::BridgeError) -> Self::Error { 40 | Error::Network 41 | } 42 | } 43 | pub fn create_routine_registry() -> RoutineRegistry { 44 | RoutineRegistry::builder() 45 | .add_query::() 46 | .add_mutation::() 47 | .build() 48 | } 49 | 50 | pub type Link = FetchLink; 51 | pub type Bridge = Bridge_; 52 | 53 | pub fn create_frontend_bridge() -> Bridge { 54 | Bridge::new(Link::builder().routines(create_routine_registry()).build()) 55 | } 56 | 57 | #[derive(Debug, PartialEq, Atom)] 58 | pub struct FrontendBridge { 59 | inner: Bridge, 60 | } 61 | 62 | impl Default for FrontendBridge { 63 | fn default() -> Self { 64 | Self { 65 | inner: Bridge::new(Link::builder().routines(create_routine_registry()).build()), 66 | } 67 | } 68 | } 69 | 70 | impl AsRef for FrontendBridge { 71 | fn as_ref(&self) -> &Bridge { 72 | &self.inner 73 | } 74 | } 75 | 76 | impl Selector for FrontendBridge { 77 | fn select(states: &bounce::BounceStates) -> std::rc::Rc { 78 | states.get_atom_value() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /templates/default/crates/api/src/routines.rs: -------------------------------------------------------------------------------- 1 | use bounce::{Atom, Selector}; 2 | use serde::{Deserialize, Serialize}; 3 | use stellation_bridge::links::FetchLink; 4 | use stellation_bridge::registry::RoutineRegistry; 5 | use stellation_bridge::routines::{BridgedMutation, BridgedQuery}; 6 | use stellation_bridge::Bridge as Bridge_; 7 | use thiserror::Error; 8 | use time::OffsetDateTime; 9 | 10 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 11 | pub struct ServerTimeQuery { 12 | pub value: OffsetDateTime, 13 | } 14 | 15 | #[derive(Debug, Error, PartialEq, Eq, Clone, Serialize, Deserialize)] 16 | pub enum Error { 17 | #[error("failed to communicate with server.")] 18 | Network, 19 | } 20 | 21 | impl BridgedQuery for ServerTimeQuery { 22 | type Error = Error; 23 | type Input = (); 24 | 25 | fn into_query_error(_e: stellation_bridge::BridgeError) -> Self::Error { 26 | Error::Network 27 | } 28 | } 29 | 30 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 31 | pub struct GreetingMutation { 32 | pub message: String, 33 | } 34 | 35 | impl BridgedMutation for GreetingMutation { 36 | type Error = Error; 37 | type Input = String; 38 | 39 | fn into_mutation_error(_e: stellation_bridge::BridgeError) -> Self::Error { 40 | Error::Network 41 | } 42 | } 43 | pub fn create_routine_registry() -> RoutineRegistry { 44 | RoutineRegistry::builder() 45 | .add_query::() 46 | .add_mutation::() 47 | .build() 48 | } 49 | 50 | pub type Link = FetchLink; 51 | pub type Bridge = Bridge_; 52 | 53 | pub fn create_frontend_bridge() -> Bridge { 54 | Bridge::new(Link::builder().routines(create_routine_registry()).build()) 55 | } 56 | 57 | #[derive(Debug, PartialEq, Atom)] 58 | pub struct FrontendBridge { 59 | inner: Bridge, 60 | } 61 | 62 | impl Default for FrontendBridge { 63 | fn default() -> Self { 64 | Self { 65 | inner: Bridge::new(Link::builder().routines(create_routine_registry()).build()), 66 | } 67 | } 68 | } 69 | 70 | impl AsRef for FrontendBridge { 71 | fn as_ref(&self) -> &Bridge { 72 | &self.inner 73 | } 74 | } 75 | 76 | impl Selector for FrontendBridge { 77 | fn select(states: &bounce::BounceStates) -> std::rc::Rc { 78 | states.get_atom_value() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/stctl/src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::Arc; 3 | 4 | use anyhow::{Context, Result}; 5 | use clap::{Parser, Subcommand}; 6 | use tokio::fs; 7 | 8 | use crate::manifest::Manifest; 9 | 10 | #[derive(Parser, Debug)] 11 | pub(crate) struct ServeCommand { 12 | /// Open browser after the development server is ready. 13 | #[arg(long)] 14 | pub open: bool, 15 | /// The name of the env profile. [Default: the same name as the build profile] 16 | #[arg(long)] 17 | pub env: Option, 18 | } 19 | 20 | #[derive(Parser, Debug)] 21 | pub(crate) struct BuildCommand { 22 | /// Build artifacts in release mode, with optimizations. 23 | #[arg(long)] 24 | pub release: bool, 25 | /// The name of the env profile. [Default: the same name as the build profile] 26 | #[arg(long)] 27 | pub env: Option, 28 | /// The build target for backend binary. [Default: the native target of the building 29 | /// environment] 30 | #[arg(long)] 31 | pub backend_target: Option, 32 | } 33 | 34 | #[derive(Subcommand, Debug)] 35 | pub(crate) enum CliCommand { 36 | /// Start the development server, serve backend and frontend, watch file changes and 37 | /// rebuild if needed. 38 | Serve(ServeCommand), 39 | /// Build the server and client for final distribution. 40 | Build(BuildCommand), 41 | /// Cleans the artifact generated by stctl, cargo and trunk. 42 | Clean, 43 | } 44 | 45 | #[derive(Parser, Debug)] 46 | #[command(author, version, about = "Command line tool for stellation.", long_about = None)] 47 | pub(crate) struct Cli { 48 | /// The path to the manifest file. 49 | /// 50 | /// If you omit this value, it will load from current working directory. 51 | #[arg(short, long, value_name = "FILE", default_value = "stellation.toml")] 52 | pub manifest_path: PathBuf, 53 | 54 | #[command(subcommand)] 55 | pub command: CliCommand, 56 | } 57 | 58 | impl Cli { 59 | pub async fn load_manifest(&self) -> Result> { 60 | let manifest_str = fs::read_to_string(&self.manifest_path).await.context( 61 | "failed to load manifest, do you have stellation.toml in the current directory?", 62 | )?; 63 | 64 | toml::from_str(&manifest_str) 65 | .map(Arc::new) 66 | .context("failed to parse stellation.toml") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /crates/stellation-backend/src/hooks.rs: -------------------------------------------------------------------------------- 1 | //! Useful hooks for stellation backend. 2 | 3 | use std::cell::RefCell; 4 | use std::fmt; 5 | use std::rc::Rc; 6 | 7 | use bounce::{use_atom_value, Atom}; 8 | use futures::future::LocalBoxFuture; 9 | use futures::{Future, FutureExt}; 10 | use yew::prelude::*; 11 | 12 | type RenderAppendHead = Box LocalBoxFuture<'static, String>>; 13 | 14 | #[derive(Atom, Clone)] 15 | pub(crate) struct HeadContents { 16 | inner: Rc>>, 17 | } 18 | 19 | impl Default for HeadContents { 20 | fn default() -> Self { 21 | panic!("Attempting to use use_append_head_content on client side rendering!"); 22 | } 23 | } 24 | 25 | impl PartialEq for HeadContents { 26 | // We never set this atom, so it will always be equal. 27 | fn eq(&self, _other: &Self) -> bool { 28 | true 29 | } 30 | } 31 | 32 | impl Eq for HeadContents {} 33 | 34 | impl HeadContents { 35 | pub(crate) fn new() -> Self { 36 | Self { 37 | inner: Rc::default(), 38 | } 39 | } 40 | 41 | pub(crate) async fn render_into(&self, w: &mut dyn fmt::Write) { 42 | for i in self.inner.take() { 43 | let _ = write!(w, "{}", i().await); 44 | } 45 | } 46 | } 47 | 48 | /// A server-side hook that appends content to head element. 49 | /// This async function is resolved after the page is completed rendered and the returned string is 50 | /// appended at the location of the ` ` comment in `index.html`, after other 51 | /// contents. 52 | /// 53 | /// # Warning 54 | /// 55 | /// The content is not managed at the client side. 56 | /// This hook is used to facility specific crates such as a CSS-in-Rust solution. 57 | /// 58 | /// If you wish to render content into the `` element, you should use 59 | /// [`bounce::helmet::Helmet`]. 60 | /// 61 | /// 62 | /// # Panics 63 | /// 64 | /// This hook should be used by a server-side only component. Panics if used in client-side 65 | /// rendering. 66 | #[hook] 67 | pub fn use_append_head_content(f: F) 68 | where 69 | F: 'static + FnOnce() -> Fut, 70 | Fut: 'static + Future, 71 | { 72 | let boxed_f: RenderAppendHead = Box::new(move || f().boxed_local()); 73 | let head_contents = use_atom_value::(); 74 | 75 | head_contents.inner.borrow_mut().push(boxed_f); 76 | } 77 | -------------------------------------------------------------------------------- /crates/stellation-backend-warp/src/filters.rs: -------------------------------------------------------------------------------- 1 | use futures::Future; 2 | use warp::path::FullPath; 3 | use warp::reject::not_found; 4 | use warp::reply::Response; 5 | use warp::{Filter, Rejection}; 6 | 7 | use crate::frontend::IndexHtml; 8 | use crate::html; 9 | use crate::request::{WarpRenderRequest, WarpRequest}; 10 | 11 | /// A filter that extracts the warp request. 12 | pub(crate) fn warp_request() -> impl Clone 13 | + Send 14 | + Filter< 15 | Extract = (WarpRequest<()>,), 16 | Error = Rejection, 17 | Future = impl Future,), Rejection>>, 18 | > { 19 | warp::path::full() 20 | .and(warp::query::raw().or_else(|_| async move { Ok::<_, Rejection>((String::new(),)) })) 21 | .and(warp::header::headers_cloned()) 22 | .then( 23 | move |path: FullPath, raw_queries: String, headers| async move { 24 | WarpRequest { 25 | path: path.into(), 26 | raw_queries: raw_queries.into(), 27 | context: ().into(), 28 | headers, 29 | } 30 | }, 31 | ) 32 | } 33 | 34 | /// A filter that extracts the warp render request. 35 | pub(crate) fn warp_render_request( 36 | index_html: IndexHtml, 37 | auto_refresh: bool, 38 | ) -> impl Clone 39 | + Send 40 | + Filter< 41 | Extract = (WarpRenderRequest<()>,), 42 | Error = Rejection, 43 | Future = impl Future,), Rejection>>, 44 | > { 45 | warp_request().then(move |req: WarpRequest<()>| { 46 | let index_html = index_html.clone(); 47 | async move { 48 | let mut template = index_html.read_content().await; 49 | 50 | if auto_refresh { 51 | template = html::add_refresh_script(&template).into(); 52 | } 53 | 54 | WarpRenderRequest { 55 | inner: req, 56 | template, 57 | is_client_only: false, 58 | } 59 | } 60 | }) 61 | } 62 | 63 | /// A filter that rejects all responses. 64 | pub(crate) fn reject() -> impl Clone 65 | + Send 66 | + Filter< 67 | Extract = (Response,), 68 | Error = Rejection, 69 | Future = impl Future>, 70 | > { 71 | warp::any().and_then(|| async move { Err::(not_found()) }) 72 | } 73 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/bridge.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::rc::Rc; 3 | 4 | use yew::prelude::*; 5 | use yew::suspense::SuspensionResult; 6 | 7 | use crate::hooks::{ 8 | use_bridged_mutation, use_bridged_query, use_bridged_query_value, UseBridgedMutationHandle, 9 | UseBridgedQueryHandle, UseBridgedQueryValueHandle, 10 | }; 11 | use crate::links::Link; 12 | use crate::routines::{BridgedMutation, BridgedQuery}; 13 | 14 | /// The Bridge. 15 | pub struct Bridge { 16 | pub(crate) link: L, 17 | } 18 | 19 | impl Clone for Bridge 20 | where 21 | L: Clone, 22 | { 23 | fn clone(&self) -> Self { 24 | Self { 25 | link: self.link.clone(), 26 | } 27 | } 28 | } 29 | 30 | impl fmt::Debug for Bridge { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | f.debug_struct("Bridge").finish_non_exhaustive() 33 | } 34 | } 35 | 36 | impl PartialEq for Bridge 37 | where 38 | L: Link, 39 | { 40 | fn eq(&self, other: &Self) -> bool { 41 | self.link == other.link 42 | } 43 | } 44 | impl Eq for Bridge where L: Link {} 45 | 46 | impl Bridge 47 | where 48 | L: Link, 49 | { 50 | /// Creates a new Bridge. 51 | pub fn new(link: L) -> Self { 52 | Self { link } 53 | } 54 | 55 | /// Returns the link used by current instance. 56 | pub fn link(&self) -> &L { 57 | &self.link 58 | } 59 | 60 | /// Bridges a mutation. 61 | pub fn use_mutation() -> impl Hook> 62 | where 63 | T: 'static + BridgedMutation, 64 | L: 'static, 65 | { 66 | use_bridged_mutation() 67 | } 68 | 69 | /// Bridges a query. 70 | pub fn use_query( 71 | input: Rc, 72 | ) -> impl Hook>> 73 | where 74 | T: 'static + BridgedQuery, 75 | L: 'static, 76 | { 77 | use_bridged_query(input) 78 | } 79 | 80 | /// Bridges a query as value. 81 | /// 82 | /// # Note 83 | /// 84 | /// This hook does not suspend the component and the data is not fetched during SSR. 85 | /// If this hook is used in SSR, this hook will remain as loading state. 86 | pub fn use_query_value( 87 | input: Rc, 88 | ) -> impl Hook> 89 | where 90 | T: 'static + BridgedQuery, 91 | L: 'static, 92 | { 93 | use_bridged_query_value(input) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/routines.rs: -------------------------------------------------------------------------------- 1 | //! Bridge Types. 2 | 3 | use std::error::Error; 4 | use std::hash::Hash; 5 | use std::marker::PhantomData; 6 | use std::rc::Rc; 7 | 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::error::BridgeError; 11 | 12 | #[cold] 13 | fn panic_network_error(e: BridgeError) -> ! { 14 | panic!("failed to communicate with server: {e:?}"); 15 | } 16 | 17 | /// A Bridged Query. 18 | /// 19 | /// This types defines a request that does not incur any side-effect on the server. 20 | /// This type is cachable and will only resolve once until refreshed. 21 | pub trait BridgedQuery: Serialize + for<'de> Deserialize<'de> + PartialEq { 22 | /// The Query Input. 23 | type Input: 'static + Serialize + for<'de> Deserialize<'de> + Hash + Eq + Clone; 24 | /// The Query Error Type. 25 | type Error: 'static + Serialize + for<'de> Deserialize<'de> + Error + PartialEq + Clone; 26 | 27 | /// Converts a BridgeError into the error type of current query. 28 | /// 29 | /// # Panics 30 | /// 31 | /// The default behaviour of a network error is panic. 32 | /// Override this method to make the error fallible. 33 | #[cold] 34 | fn into_query_error(e: BridgeError) -> Self::Error { 35 | panic_network_error(e); 36 | } 37 | } 38 | 39 | /// The query result type. 40 | pub type QueryResult = std::result::Result, ::Error>; 41 | 42 | /// A Bridged Mutation. 43 | /// 44 | /// This types defines a request that incur side-effects on the server / cannot be cached. 45 | pub trait BridgedMutation: Serialize + for<'de> Deserialize<'de> + PartialEq { 46 | /// The Mutation Input. 47 | type Input: 'static + Serialize + for<'de> Deserialize<'de>; 48 | /// The Mutation Error. 49 | type Error: 'static + Serialize + for<'de> Deserialize<'de> + Error + PartialEq + Clone; 50 | 51 | /// Converts a BridgeError into the error type of current mutation. 52 | /// 53 | /// # Panics 54 | /// 55 | /// The default behaviour of a network error is panic. 56 | /// Override this method to make the error fallible. 57 | #[cold] 58 | fn into_mutation_error(e: BridgeError) -> Self::Error { 59 | panic_network_error(e); 60 | } 61 | } 62 | 63 | /// The mutation result type. 64 | pub type MutationResult = std::result::Result, ::Error>; 65 | 66 | /// A placeholder type until never type lands in std. 67 | #[derive(thiserror::Error, Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 68 | #[error("this never happens")] 69 | pub struct Never(PhantomData<()>); 70 | -------------------------------------------------------------------------------- /crates/stellation-backend-cli/src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::net::ToSocketAddrs; 3 | use std::path::PathBuf; 4 | 5 | use anyhow::{anyhow, Context}; 6 | use clap::Parser; 7 | use stellation_backend::ServerAppProps; 8 | use stellation_backend_tower::{Frontend, Server, TowerEndpoint, TowerRenderRequest}; 9 | use stellation_bridge::links::{Link, PhantomLink}; 10 | use stellation_core::dev::StctlMetadata; 11 | use typed_builder::TypedBuilder; 12 | use yew::BaseComponent; 13 | 14 | #[derive(Parser)] 15 | struct Arguments { 16 | /// The address to listen to. 17 | #[arg(long, default_value = "localhost:5000", env = "STELLATION_LISTEN_ADDR")] 18 | listen_addr: String, 19 | /// The ditectory that contains the frontend artifact. 20 | #[arg(long, env = "STELLATION_FRONTEND_DIR")] 21 | frontend_dir: Option, 22 | } 23 | 24 | /// The default command line instance for the backend server. 25 | #[derive(Debug, TypedBuilder)] 26 | pub struct Cli 27 | where 28 | COMP: BaseComponent, 29 | { 30 | endpoint: TowerEndpoint, 31 | } 32 | 33 | impl Cli 34 | where 35 | COMP: BaseComponent>>, 36 | CTX: 'static, 37 | L: 'static + Link, 38 | { 39 | /// Parses the arguments and runs the server. 40 | pub async fn run(self) -> anyhow::Result<()> { 41 | let Self { mut endpoint } = self; 42 | 43 | let args = Arguments::parse(); 44 | 45 | // Prioritise information from stctl. 46 | let meta = match env::var(StctlMetadata::ENV_NAME) { 47 | Ok(m) => Some(StctlMetadata::from_json(&m).context("failed to load metadata")?), 48 | Err(_) => None, 49 | }; 50 | 51 | let addr = meta 52 | .as_ref() 53 | .map(|m| m.listen_addr.as_str()) 54 | .unwrap_or_else(|| args.listen_addr.as_str()); 55 | 56 | if let Some(ref p) = args.frontend_dir { 57 | endpoint = endpoint.with_frontend(Frontend::new_path(p)); 58 | } 59 | 60 | if let Some(ref meta) = meta { 61 | endpoint = endpoint 62 | .with_frontend(Frontend::new_path(&meta.frontend_dev_build_dir)) 63 | .with_auto_refresh(); 64 | } 65 | 66 | let listen_addr = addr 67 | .to_socket_addrs() 68 | .context("failed to parse address") 69 | .and_then(|m| { 70 | m.into_iter() 71 | .next() 72 | .ok_or_else(|| anyhow!("failed to parse address")) 73 | })?; 74 | 75 | tracing::info!("Listening at: http://{}/", addr); 76 | 77 | Server::<()>::bind(listen_addr) 78 | .serve_service(endpoint.into_tower_service()) 79 | .await?; 80 | 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/links/local_link.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::AtomicUsize; 2 | use std::sync::Arc; 3 | 4 | use async_trait::async_trait; 5 | use futures::{future, FutureExt, TryFutureExt}; 6 | use typed_builder::TypedBuilder; 7 | 8 | use super::Link; 9 | use crate::registry::{ResolverRegistry, RoutineRegistry}; 10 | use crate::routines::{BridgedMutation, BridgedQuery, MutationResult, QueryResult}; 11 | use crate::BridgeResult; 12 | 13 | /// A Link that resolves routine with local resolvers. 14 | /// 15 | /// This is usually used to implement SSR or a backend server. 16 | #[derive(TypedBuilder, Debug)] 17 | pub struct LocalLink { 18 | /// The routine registry for all registered routines. 19 | routines: RoutineRegistry, 20 | /// The routine registry for all registered resolvers. 21 | resolvers: ResolverRegistry, 22 | 23 | /// The bridge context 24 | #[builder(setter(into))] 25 | context: Arc, 26 | 27 | /// The link equity tracker. 28 | #[builder(setter(skip), default_code = r#"LocalLink::<()>::next_id()"#)] 29 | id: usize, 30 | } 31 | 32 | impl PartialEq for LocalLink { 33 | fn eq(&self, other: &Self) -> bool { 34 | self.id == other.id 35 | } 36 | } 37 | 38 | impl Clone for LocalLink { 39 | fn clone(&self) -> Self { 40 | Self { 41 | routines: self.routines.clone(), 42 | resolvers: self.resolvers.clone(), 43 | context: self.context.clone(), 44 | id: self.id, 45 | } 46 | } 47 | } 48 | 49 | impl LocalLink { 50 | fn next_id() -> usize { 51 | static ID: AtomicUsize = AtomicUsize::new(0); 52 | 53 | ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst) 54 | } 55 | } 56 | 57 | #[async_trait(?Send)] 58 | impl Link for LocalLink { 59 | async fn resolve_encoded(&self, input_buf: &[u8]) -> BridgeResult> { 60 | self.resolvers 61 | .resolve_encoded(&self.context, input_buf) 62 | .await 63 | } 64 | 65 | async fn resolve_query(&self, input: &T::Input) -> QueryResult 66 | where 67 | T: 'static + BridgedQuery, 68 | { 69 | future::ready(input) 70 | .map(|m| self.routines.encode_query_input::(m)) 71 | .and_then(|m| async move { self.resolvers.resolve_encoded(&self.context, &m).await }) 72 | .map_err(T::into_query_error) 73 | .and_then(|m| async move { self.routines.decode_query_output::(&m) }) 74 | .await 75 | } 76 | 77 | async fn resolve_mutation(&self, input: &T::Input) -> MutationResult 78 | where 79 | T: 'static + BridgedMutation, 80 | { 81 | future::ready(input) 82 | .map(|m| self.routines.encode_mutation_input::(m)) 83 | .and_then(|m| async move { self.resolvers.resolve_encoded(&self.context, &m).await }) 84 | .map_err(T::into_mutation_error) 85 | .and_then(|m| async move { self.routines.decode_mutation_output::(&m) }) 86 | .await 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crates/stellation-backend/src/root.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use anymap2::AnyMap; 4 | use bounce::helmet::{HelmetBridge, StaticWriter}; 5 | use bounce::BounceRoot; 6 | use stellation_bridge::links::Link; 7 | use stellation_bridge::state::BridgeState; 8 | use stellation_bridge::Bridge; 9 | use yew::prelude::*; 10 | use yew_router::history::{AnyHistory, History, MemoryHistory}; 11 | use yew_router::Router; 12 | 13 | use crate::hooks::HeadContents; 14 | use crate::props::ServerAppProps; 15 | use crate::Request; 16 | 17 | #[derive(Properties)] 18 | pub(crate) struct StellationRootProps 19 | where 20 | L: Link, 21 | { 22 | pub helmet_writer: StaticWriter, 23 | pub server_app_props: ServerAppProps, 24 | pub bridge: Option>, 25 | pub head_contents: HeadContents, 26 | } 27 | 28 | impl PartialEq for StellationRootProps 29 | where 30 | L: Link, 31 | { 32 | fn eq(&self, other: &Self) -> bool { 33 | self.helmet_writer == other.helmet_writer 34 | && self.server_app_props == other.server_app_props 35 | && self.bridge == other.bridge 36 | } 37 | } 38 | 39 | impl Clone for StellationRootProps 40 | where 41 | L: Link, 42 | { 43 | fn clone(&self) -> Self { 44 | Self { 45 | helmet_writer: self.helmet_writer.clone(), 46 | server_app_props: self.server_app_props.clone(), 47 | bridge: self.bridge.clone(), 48 | head_contents: self.head_contents.clone(), 49 | } 50 | } 51 | } 52 | 53 | #[function_component] 54 | pub(crate) fn StellationRoot(props: &StellationRootProps) -> Html 55 | where 56 | COMP: BaseComponent>, 57 | REQ: 'static + Request, 58 | CTX: 'static, 59 | L: 'static + Link, 60 | { 61 | let StellationRootProps { 62 | helmet_writer, 63 | server_app_props, 64 | bridge, 65 | head_contents, 66 | .. 67 | } = props.clone(); 68 | 69 | let get_init_states = use_callback( 70 | move |_, bridge| { 71 | let mut states = AnyMap::new(); 72 | states.insert(head_contents.clone()); 73 | if let Some(m) = bridge.clone().map(BridgeState::from_bridge) { 74 | states.insert(m); 75 | } 76 | 77 | states 78 | }, 79 | bridge, 80 | ); 81 | 82 | let history: AnyHistory = MemoryHistory::new().into(); 83 | history 84 | .push_with_query( 85 | server_app_props.path(), 86 | server_app_props 87 | .queries::, Cow<'_, str>)>>() 88 | .expect("failed to parse queries"), 89 | ) 90 | .expect("failed to push path."); 91 | 92 | let children = html! { }; 93 | 94 | html! { 95 | 96 | 97 | 98 | {children} 99 | 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/stellation-backend-tower/src/endpoint.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::future::Future; 3 | 4 | use hyper::{Body, Request, Response}; 5 | use stellation_backend::ServerAppProps; 6 | use stellation_backend_warp::{Frontend, WarpEndpoint}; 7 | use stellation_bridge::links::{Link, PhantomLink}; 8 | use stellation_bridge::Bridge; 9 | use tower::Service; 10 | use yew::BaseComponent; 11 | 12 | use crate::{TowerRenderRequest, TowerRequest}; 13 | 14 | /// Creates a stellation endpoint that can be turned into a tower service. 15 | /// 16 | /// This endpoint serves bridge requests and frontend requests. 17 | /// You can turn this type into a tower service by calling 18 | /// [`into_tower_service()`](Self::into_tower_service). 19 | #[derive(Debug)] 20 | pub struct TowerEndpoint 21 | where 22 | COMP: BaseComponent, 23 | { 24 | inner: WarpEndpoint, 25 | } 26 | 27 | impl Default for TowerEndpoint 28 | where 29 | COMP: BaseComponent, 30 | CTX: 'static + Default, 31 | { 32 | fn default() -> Self { 33 | Self::new() 34 | } 35 | } 36 | 37 | impl TowerEndpoint 38 | where 39 | COMP: BaseComponent, 40 | CTX: 'static, 41 | { 42 | /// Creates an endpoint. 43 | pub fn new() -> Self 44 | where 45 | CTX: Default, 46 | { 47 | Self { 48 | inner: WarpEndpoint::default(), 49 | } 50 | } 51 | } 52 | 53 | impl TowerEndpoint 54 | where 55 | COMP: BaseComponent, 56 | CTX: 'static, 57 | { 58 | /// Appends a context to current request. 59 | pub fn with_append_context(self, append_context: F) -> TowerEndpoint 60 | where 61 | F: 'static + Clone + Send + Fn(TowerRenderRequest<()>) -> Fut, 62 | Fut: 'static + Future>, 63 | C: 'static, 64 | { 65 | TowerEndpoint { 66 | inner: self.inner.with_append_context(append_context), 67 | } 68 | } 69 | 70 | /// Appends a bridge to current request. 71 | pub fn with_create_bridge( 72 | self, 73 | create_bridge: F, 74 | ) -> TowerEndpoint 75 | where 76 | F: 'static + Clone + Send + Fn(TowerRequest<()>) -> Fut, 77 | Fut: 'static + Future>, 78 | LINK: 'static + Link, 79 | { 80 | TowerEndpoint { 81 | inner: self.inner.with_create_bridge(create_bridge), 82 | } 83 | } 84 | 85 | /// Enables auto refresh. 86 | /// 87 | /// This is useful during development. 88 | pub fn with_auto_refresh(mut self) -> Self { 89 | self.inner = self.inner.with_auto_refresh(); 90 | self 91 | } 92 | 93 | /// Serves a frontend with current endpoint. 94 | pub fn with_frontend(mut self, frontend: Frontend) -> Self { 95 | self.inner = self.inner.with_frontend(frontend); 96 | self 97 | } 98 | 99 | /// Creates a tower service from current endpoint. 100 | pub fn into_tower_service( 101 | self, 102 | ) -> impl 'static 103 | + Clone 104 | + Service< 105 | Request, 106 | Response = Response, 107 | Error = Infallible, 108 | Future = impl 'static + Send + Future, Infallible>>, 109 | > 110 | where 111 | COMP: BaseComponent>>, 112 | CTX: 'static, 113 | L: 'static + Link, 114 | { 115 | let routes = self.inner.into_warp_filter(); 116 | warp::service(routes) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /crates/stellation-backend-warp/src/request.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use http::HeaderMap; 4 | use stellation_backend::{RenderRequest, Request}; 5 | use warp::path::FullPath; 6 | 7 | /// A stellation request with information extracted from a warp request, used by 8 | /// server-side-rendering. 9 | #[derive(Debug)] 10 | pub struct WarpRenderRequest { 11 | pub(crate) inner: WarpRequest, 12 | pub(crate) template: Arc, 13 | pub(crate) is_client_only: bool, 14 | } 15 | 16 | impl Clone for WarpRenderRequest { 17 | fn clone(&self) -> Self { 18 | Self { 19 | inner: self.inner.clone(), 20 | template: self.template.clone(), 21 | is_client_only: self.is_client_only, 22 | } 23 | } 24 | } 25 | 26 | impl Request for WarpRenderRequest { 27 | type Context = CTX; 28 | 29 | fn path(&self) -> &str { 30 | self.inner.path() 31 | } 32 | 33 | fn raw_queries(&self) -> &str { 34 | self.inner.raw_queries() 35 | } 36 | 37 | fn context(&self) -> &Self::Context { 38 | self.inner.context() 39 | } 40 | 41 | fn headers(&self) -> &HeaderMap { 42 | self.inner.headers() 43 | } 44 | } 45 | 46 | impl RenderRequest for WarpRenderRequest { 47 | fn template(&self) -> &str { 48 | self.template.as_ref() 49 | } 50 | 51 | fn is_client_only(&self) -> bool { 52 | self.is_client_only 53 | } 54 | } 55 | 56 | impl WarpRenderRequest { 57 | /// Appends a context to current server app to help resolving the request. 58 | pub fn with_context(self, context: C) -> WarpRenderRequest { 59 | WarpRenderRequest { 60 | template: self.template, 61 | inner: self.inner.with_context(context), 62 | is_client_only: self.is_client_only, 63 | } 64 | } 65 | 66 | pub(crate) fn into_inner(self) -> WarpRequest { 67 | self.inner 68 | } 69 | 70 | /// Marks this request to be rendered at the client side. 71 | pub fn client_only(mut self) -> Self { 72 | self.is_client_only = true; 73 | 74 | self 75 | } 76 | } 77 | 78 | /// A stellation request with information extracted from a warp request, used by 79 | /// server-side-rendering. 80 | #[derive(Debug)] 81 | pub struct WarpRequest { 82 | pub(crate) path: Arc, 83 | pub(crate) raw_queries: Arc, 84 | pub(crate) context: Arc, 85 | pub(crate) headers: HeaderMap, 86 | } 87 | 88 | impl Clone for WarpRequest { 89 | fn clone(&self) -> Self { 90 | Self { 91 | path: self.path.clone(), 92 | raw_queries: self.raw_queries.clone(), 93 | context: self.context.clone(), 94 | headers: self.headers.clone(), 95 | } 96 | } 97 | } 98 | 99 | impl Request for WarpRequest { 100 | type Context = CTX; 101 | 102 | fn path(&self) -> &str { 103 | self.path.as_str() 104 | } 105 | 106 | fn raw_queries(&self) -> &str { 107 | &self.raw_queries 108 | } 109 | 110 | fn context(&self) -> &Self::Context { 111 | &self.context 112 | } 113 | 114 | fn headers(&self) -> &HeaderMap { 115 | &self.headers 116 | } 117 | } 118 | 119 | impl WarpRequest { 120 | /// Appends a context to current server app to help resolving the request. 121 | pub fn with_context(self, context: C) -> WarpRequest { 122 | WarpRequest { 123 | path: self.path, 124 | raw_queries: self.raw_queries, 125 | headers: self.headers, 126 | context: context.into(), 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/links/fetch_link.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | use async_trait::async_trait; 4 | use futures::{future, FutureExt, TryFutureExt}; 5 | use gloo_net::http::Request; 6 | use js_sys::Uint8Array; 7 | use typed_builder::TypedBuilder; 8 | 9 | use super::Link; 10 | use crate::registry::RoutineRegistry; 11 | use crate::routines::{BridgedMutation, BridgedQuery, MutationResult, QueryResult}; 12 | use crate::{BridgeError, BridgeResult}; 13 | 14 | /// A Link implemented with `fetch`, this requires a WebAssembly target with available global 15 | /// `fetch`. 16 | /// 17 | /// # Example 18 | /// 19 | /// ``` 20 | /// # use crate::links::FetchLink; 21 | /// # use crate::registry::RoutineRegistry; 22 | /// # let routines = RoutineRegistry::builder().build(); 23 | /// let link = FetchLink::builder() 24 | /// .url("/_bridge") // Defaults to `/_bridge`, which is also default on most first party implementations. 25 | /// .routines(routines) 26 | /// .build(); 27 | /// ``` 28 | #[derive(TypedBuilder, Debug, Clone)] 29 | pub struct FetchLink { 30 | /// The bridge URL, defaults to `/_bridge`, which is also the default used by official backend 31 | /// implementations. 32 | #[builder(setter(into), default_code = r#""/_bridge".to_string()"#)] 33 | url: String, 34 | /// The routine registry for all registered routines. 35 | routines: RoutineRegistry, 36 | /// The bearer token to send to the server. 37 | #[builder(setter(into, strip_option), default)] 38 | token: Option, 39 | 40 | /// The link equity tracker. 41 | #[builder(setter(skip), default_code = r#"FetchLink::next_id()"#)] 42 | id: usize, 43 | } 44 | 45 | impl PartialEq for FetchLink { 46 | fn eq(&self, other: &Self) -> bool { 47 | self.id == other.id 48 | } 49 | } 50 | 51 | impl FetchLink { 52 | fn next_id() -> usize { 53 | thread_local! { 54 | static ID: Cell = Cell::new(0); 55 | } 56 | 57 | ID.with(|m| { 58 | m.set(m.get() + 1); 59 | 60 | m.get() 61 | }) 62 | } 63 | } 64 | 65 | #[async_trait(?Send)] 66 | impl Link for FetchLink { 67 | async fn resolve_encoded(&self, input_buf: &[u8]) -> BridgeResult> { 68 | future::ready(self.url.as_str()) 69 | .map(Request::post) 70 | .map(|m| m.header("content-type", "application/x-bincode")) 71 | .map(|req| { 72 | if let Some(ref m) = self.token { 73 | return req.header("authorization", &format!("Bearer {m}")); 74 | } 75 | 76 | req 77 | }) 78 | .map(move |m| m.body(&Uint8Array::from(input_buf))) 79 | .and_then(|m| m.send()) 80 | .and_then(|m| async move { m.binary().await }) 81 | .map_err(BridgeError::Network) 82 | .await 83 | } 84 | 85 | async fn resolve_query(&self, input: &T::Input) -> QueryResult 86 | where 87 | T: 'static + BridgedQuery, 88 | { 89 | future::ready(input) 90 | .map(|m| self.routines.encode_query_input::(m)) 91 | .and_then(|m| async move { self.resolve_encoded(&m).await }) 92 | .map_err(T::into_query_error) 93 | .and_then(|m| async move { self.routines.decode_query_output::(&m) }) 94 | .await 95 | } 96 | 97 | async fn resolve_mutation(&self, input: &T::Input) -> MutationResult 98 | where 99 | T: 'static + BridgedMutation, 100 | { 101 | future::ready(input) 102 | .map(|m| self.routines.encode_mutation_input::(m)) 103 | .and_then(|m| async move { self.resolve_encoded(&m).await }) 104 | .map_err(T::into_mutation_error) 105 | .and_then(|m| async move { self.routines.decode_mutation_output::(&m) }) 106 | .await 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crates/stellation-backend/src/renderer.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Write; 3 | use std::marker::PhantomData; 4 | use std::rc::Rc; 5 | 6 | use bounce::helmet::render_static; 7 | use stellation_bridge::links::{Link, PhantomLink}; 8 | use stellation_bridge::Bridge; 9 | use yew::BaseComponent; 10 | 11 | use crate::hooks::HeadContents; 12 | use crate::request::RenderRequest; 13 | use crate::root::{StellationRoot, StellationRootProps}; 14 | use crate::{html, ServerAppProps}; 15 | 16 | /// The Stellation Backend Renderer. 17 | /// 18 | /// This type wraps the [Yew Server Renderer](yew::ServerRenderer) and provides additional features. 19 | /// 20 | /// # Note 21 | /// 22 | /// Stellation provides [`BrowserRouter`](yew_router::BrowserRouter) and 23 | /// [`BounceRoot`](bounce::BounceRoot) to all applications. 24 | /// 25 | /// Bounce Helmet is also bridged automatically. 26 | /// 27 | /// You do not need to add them manually. 28 | pub struct ServerRenderer 29 | where 30 | COMP: BaseComponent, 31 | { 32 | request: REQ, 33 | bridge: Option>, 34 | _marker: PhantomData<(COMP, REQ, CTX)>, 35 | } 36 | 37 | impl fmt::Debug for ServerRenderer 38 | where 39 | COMP: BaseComponent, 40 | { 41 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | f.write_str("ServerRenderer<_>") 43 | } 44 | } 45 | 46 | impl ServerRenderer 47 | where 48 | COMP: BaseComponent>, 49 | { 50 | /// Creates a Renderer with specified request. 51 | pub fn new(request: REQ) -> ServerRenderer { 52 | ServerRenderer { 53 | request, 54 | bridge: None, 55 | _marker: PhantomData, 56 | } 57 | } 58 | } 59 | 60 | impl ServerRenderer 61 | where 62 | COMP: BaseComponent>, 63 | { 64 | /// Connects a bridge to the application. 65 | pub fn bridge(self, bridge: Bridge) -> ServerRenderer { 66 | ServerRenderer { 67 | request: self.request, 68 | bridge: Some(bridge), 69 | _marker: PhantomData, 70 | } 71 | } 72 | 73 | /// Renders the application. 74 | /// 75 | /// # Note: 76 | /// 77 | /// This future is `!Send`. 78 | pub async fn render(self) -> String 79 | where 80 | CTX: 'static, 81 | REQ: 'static, 82 | L: 'static + Link, 83 | REQ: RenderRequest, 84 | { 85 | let Self { 86 | bridge, request, .. 87 | } = self; 88 | 89 | let mut head_s = String::new(); 90 | let mut helmet_tags = Vec::new(); 91 | let mut body_s = String::new(); 92 | 93 | let request: Rc<_> = request.into(); 94 | 95 | if !request.is_client_only() { 96 | let head_contents = HeadContents::new(); 97 | 98 | let (reader, writer) = render_static(); 99 | 100 | let props = ServerAppProps::from_request(request.clone()); 101 | 102 | body_s = yew::LocalServerRenderer::>::with_props( 103 | StellationRootProps { 104 | server_app_props: props, 105 | helmet_writer: writer, 106 | bridge, 107 | head_contents: head_contents.clone(), 108 | }, 109 | ) 110 | .render() 111 | .await; 112 | 113 | helmet_tags.append(&mut reader.render().await); 114 | let _ = write!( 115 | &mut head_s, 116 | r#""# 117 | ); 118 | 119 | head_contents.render_into(&mut head_s).await; 120 | } 121 | 122 | html::format_html(request.template(), helmet_tags, head_s, body_s).await 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /crates/stellation-frontend/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Stellation Frontend. 2 | //! 3 | //! This crate contains the frontend renderer and useful utilities for stellation applications. 4 | 5 | #![deny(clippy::all)] 6 | #![deny(missing_debug_implementations)] 7 | #![deny(unsafe_code)] 8 | #![deny(non_snake_case)] 9 | #![deny(clippy::cognitive_complexity)] 10 | #![deny(missing_docs)] 11 | #![cfg_attr(documenting, feature(doc_cfg))] 12 | #![cfg_attr(documenting, feature(doc_auto_cfg))] 13 | #![cfg_attr(any(releasing, not(debug_assertions)), deny(dead_code, unused_imports))] 14 | 15 | use std::marker::PhantomData; 16 | 17 | use bounce::Selector; 18 | use stellation_bridge::links::{Link, PhantomLink}; 19 | use stellation_bridge::state::BridgeState; 20 | use stellation_bridge::Bridge; 21 | use yew::prelude::*; 22 | 23 | use crate::root::{StellationRoot, StellationRootProps}; 24 | pub mod components; 25 | mod root; 26 | pub mod trace; 27 | 28 | /// The Stellation Frontend Renderer. 29 | /// 30 | /// This type wraps the [Yew Renderer](yew::Renderer) and provides additional features. 31 | /// 32 | /// # Note 33 | /// 34 | /// Stellation provides [`BrowserRouter`](yew_router::BrowserRouter) and 35 | /// [`BounceRoot`](bounce::BounceRoot) to all applications. 36 | /// 37 | /// Bounce Helmet is also bridged automatically. 38 | /// 39 | /// You do not need to add them manually. 40 | #[derive(Debug)] 41 | pub struct Renderer 42 | where 43 | COMP: BaseComponent, 44 | L: Link, 45 | { 46 | props: COMP::Properties, 47 | bridge_state: Option>, 48 | _marker: PhantomData, 49 | } 50 | 51 | impl Default for Renderer 52 | where 53 | COMP: BaseComponent, 54 | COMP::Properties: Default, 55 | { 56 | fn default() -> Self { 57 | Self::new() 58 | } 59 | } 60 | 61 | impl Renderer 62 | where 63 | COMP: BaseComponent, 64 | { 65 | /// Creates a Renderer with default props. 66 | pub fn new() -> Renderer 67 | where 68 | COMP::Properties: Default, 69 | { 70 | Self::with_props(Default::default()) 71 | } 72 | } 73 | 74 | impl Renderer 75 | where 76 | COMP: BaseComponent, 77 | L: 'static + Link, 78 | { 79 | /// Creates a Renderer with specified props. 80 | pub fn with_props(props: COMP::Properties) -> Renderer { 81 | Renderer { 82 | props, 83 | bridge_state: None, 84 | _marker: PhantomData, 85 | } 86 | } 87 | 88 | /// Connects a bridge to the application. 89 | pub fn bridge_selector(self) -> Renderer 90 | where 91 | S: 'static + Selector + AsRef>, 92 | LINK: 'static + Link, 93 | { 94 | Renderer { 95 | props: self.props, 96 | bridge_state: Some(BridgeState::from_bridge_selector::()), 97 | _marker: PhantomData, 98 | } 99 | } 100 | 101 | fn into_yew_renderer(self) -> yew::Renderer> { 102 | let Self { 103 | props, 104 | bridge_state, 105 | .. 106 | } = self; 107 | 108 | let children = html! { 109 | 110 | }; 111 | 112 | let props = StellationRootProps { 113 | bridge_state, 114 | children, 115 | }; 116 | 117 | yew::Renderer::with_props(props) 118 | } 119 | 120 | /// Renders the application. 121 | /// 122 | /// Whether the application is rendered or hydrated is determined automatically based on whether 123 | /// SSR is used on the server side for this page. 124 | pub fn render(self) { 125 | let renderer = self.into_yew_renderer(); 126 | 127 | if web_sys::window() 128 | .and_then(|m| m.document()) 129 | .and_then(|m| { 130 | m.query_selector(r#"meta[name="stellation-mode"][content="hydrate"]"#) 131 | .ok() 132 | .flatten() 133 | }) 134 | .is_some() 135 | { 136 | renderer.hydrate(); 137 | } else { 138 | renderer.render(); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /crates/stellation-backend-warp/src/frontend.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | use std::path::{Path, PathBuf}; 3 | use std::sync::Arc; 4 | use std::{fmt, str}; 5 | 6 | use rust_embed::{EmbeddedFile, RustEmbed}; 7 | use stellation_backend::utils::ThreadLocalLazy; 8 | use tokio::fs; 9 | use warp::filters::fs::File; 10 | use warp::filters::BoxedFilter; 11 | use warp::path::Tail; 12 | use warp::reply::{with_header, Response}; 13 | use warp::{Filter, Rejection, Reply}; 14 | 15 | type GetFileFn = Box Option>; 16 | 17 | type GetFile = ThreadLocalLazy; 18 | 19 | #[derive(Clone)] 20 | enum Inner { 21 | Path(PathBuf), 22 | Embed { get_file: GetFile }, 23 | } 24 | 25 | impl fmt::Debug for Inner { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | match self { 28 | Inner::Path(ref p) => f.debug_struct("Inner::Path").field("0", p).finish(), 29 | Inner::Embed { .. } => f.debug_struct("Inner::Embed").finish_non_exhaustive(), 30 | } 31 | } 32 | } 33 | 34 | /// The frontend provider. 35 | /// 36 | /// This type defines how the frontend is served by the server. 37 | #[derive(Debug, Clone)] 38 | pub struct Frontend { 39 | inner: Inner, 40 | } 41 | 42 | impl Frontend { 43 | /// Serves the frontend from a directory in the filesystem. 44 | pub fn new_path

(p: P) -> Self 45 | where 46 | P: Into, 47 | { 48 | let p = p.into(); 49 | 50 | Self { 51 | inner: Inner::Path(p), 52 | } 53 | } 54 | 55 | /// Serves the frontend from a RustEmbed instance. 56 | pub fn new_embedded() -> Self 57 | where 58 | E: RustEmbed, 59 | { 60 | let get_file = ThreadLocalLazy::new(|| Box::new(|path: &str| E::get(path)) as GetFileFn); 61 | 62 | Self { 63 | inner: Inner::Embed { get_file }, 64 | } 65 | } 66 | 67 | pub(crate) fn into_warp_filter(self) -> BoxedFilter<(Response,)> { 68 | match self.inner { 69 | Inner::Path(m) => warp::fs::dir(m) 70 | .then(|m: File| async move { m.into_response() }) 71 | .boxed(), 72 | Inner::Embed { get_file } => warp::path::tail() 73 | .and_then(move |path: Tail| { 74 | let get_file = get_file.clone(); 75 | async move { 76 | let get_file = get_file.deref(); 77 | 78 | let asset = get_file(path.as_str()).ok_or_else(warp::reject::not_found)?; 79 | let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream(); 80 | 81 | Ok::<_, Rejection>( 82 | with_header( 83 | warp::hyper::Response::new(asset.data), 84 | "content-type", 85 | mime.as_ref(), 86 | ) 87 | .into_response(), 88 | ) 89 | } 90 | }) 91 | .boxed(), 92 | } 93 | } 94 | 95 | pub(crate) fn index_html(&self) -> IndexHtml { 96 | match self.inner { 97 | Inner::Path(ref m) => IndexHtml::Path(m.join("index.html").into()), 98 | Inner::Embed { ref get_file } => (get_file.deref())("index.html") 99 | .map(|m| m.data) 100 | .as_deref() 101 | .map(String::from_utf8_lossy) 102 | .map(Arc::from) 103 | .map(IndexHtml::Embedded) 104 | .expect("index.html not found!"), 105 | } 106 | } 107 | } 108 | 109 | #[derive(Clone)] 110 | pub(crate) enum IndexHtml { 111 | Embedded(Arc), 112 | Path(Arc), 113 | } 114 | 115 | impl IndexHtml { 116 | pub async fn read_content(&self) -> Arc { 117 | match self { 118 | IndexHtml::Path(p) => fs::read_to_string(&p) 119 | .await 120 | .map(Arc::from) 121 | .expect("TODO: implement failure."), 122 | 123 | IndexHtml::Embedded(ref s) => s.clone(), 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/state.rs: -------------------------------------------------------------------------------- 1 | //! States used by the frontend and backend. 2 | //! 3 | //! These states are registered automatically if you use backend endpoint or frontend renderer. 4 | 5 | use std::fmt; 6 | use std::ops::Deref; 7 | use std::rc::Rc; 8 | 9 | use bounce::{Atom, BounceStates, Selector}; 10 | 11 | use crate::links::Link; 12 | use crate::Bridge; 13 | 14 | type SelectBridge = Rc Bridge>; 15 | 16 | enum BridgeStateVariant 17 | where 18 | L: Link, 19 | { 20 | Value(Bridge), 21 | Selector(SelectBridge), 22 | } 23 | 24 | impl fmt::Debug for BridgeStateVariant 25 | where 26 | L: Link, 27 | { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | f.debug_struct("BridgeStateVariant<_>") 30 | .finish_non_exhaustive() 31 | } 32 | } 33 | 34 | impl PartialEq for BridgeStateVariant 35 | where 36 | L: Link, 37 | { 38 | // We allow this implementation for now as we do not expect this state to change after it is 39 | // declared. 40 | #[allow(clippy::vtable_address_comparisons)] 41 | fn eq(&self, other: &Self) -> bool { 42 | match (self, other) { 43 | (Self::Value(ref l), Self::Value(ref r)) => l == r, 44 | (Self::Selector(ref l), Self::Selector(ref r)) => Rc::ptr_eq(l, r), 45 | _ => false, 46 | } 47 | } 48 | } 49 | 50 | impl Eq for BridgeStateVariant where L: Link {} 51 | impl Clone for BridgeStateVariant 52 | where 53 | L: Link, 54 | { 55 | fn clone(&self) -> Self { 56 | match self { 57 | Self::Value(v) => Self::Value(v.clone()), 58 | Self::Selector(s) => Self::Selector(s.clone()), 59 | } 60 | } 61 | } 62 | 63 | /// The bridge state. 64 | #[derive(Atom, Debug)] 65 | pub struct BridgeState 66 | where 67 | L: Link, 68 | { 69 | /// The bridge stored in the state. 70 | inner: BridgeStateVariant, 71 | } 72 | 73 | impl Default for BridgeState 74 | where 75 | L: Link, 76 | { 77 | fn default() -> Self { 78 | panic!("bridge is not initialised!") 79 | } 80 | } 81 | 82 | impl PartialEq for BridgeState 83 | where 84 | L: Link, 85 | { 86 | fn eq(&self, other: &Self) -> bool { 87 | self.inner == other.inner 88 | } 89 | } 90 | 91 | impl Clone for BridgeState 92 | where 93 | L: Link, 94 | { 95 | fn clone(&self) -> Self { 96 | Self { 97 | inner: self.inner.clone(), 98 | } 99 | } 100 | } 101 | 102 | impl Eq for BridgeState where L: Link {} 103 | 104 | impl BridgeState 105 | where 106 | L: Link, 107 | { 108 | /// Creates a Bridge State from a bridge value 109 | pub fn from_bridge(bridge: Bridge) -> Self { 110 | Self { 111 | inner: BridgeStateVariant::Value(bridge), 112 | } 113 | } 114 | 115 | /// Creates a Bridge State from a bridge selector 116 | pub fn from_bridge_selector() -> Self 117 | where 118 | S: 'static + Selector + AsRef>, 119 | { 120 | Self { 121 | inner: BridgeStateVariant::Selector(Rc::new(|states: &BounceStates| { 122 | states.get_selector_value::().as_ref().as_ref().clone() 123 | })), 124 | } 125 | } 126 | } 127 | 128 | pub(crate) struct BridgeSelector 129 | where 130 | L: Link, 131 | { 132 | inner: Bridge, 133 | } 134 | 135 | impl PartialEq for BridgeSelector 136 | where 137 | L: Link, 138 | { 139 | fn eq(&self, other: &Self) -> bool { 140 | self.inner == other.inner 141 | } 142 | } 143 | 144 | impl Eq for BridgeSelector where L: Link {} 145 | 146 | impl Selector for BridgeSelector 147 | where 148 | L: 'static + Link, 149 | { 150 | fn select(states: &BounceStates) -> Rc { 151 | let state = states.get_atom_value::>(); 152 | 153 | match state.inner { 154 | BridgeStateVariant::Selector(ref s) => Self { inner: s(states) }, 155 | BridgeStateVariant::Value(ref v) => Self { inner: v.clone() }, 156 | } 157 | .into() 158 | } 159 | } 160 | 161 | impl Deref for BridgeSelector 162 | where 163 | L: Link, 164 | { 165 | type Target = Bridge; 166 | 167 | fn deref(&self) -> &Self::Target { 168 | &self.inner 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /crates/stellation-backend-cli/src/trace.rs: -------------------------------------------------------------------------------- 1 | //! Tracing support. 2 | 3 | use std::env; 4 | 5 | use console::style; 6 | use stellation_core::dev::StctlMetadata; 7 | use tracing::field::Visit; 8 | use tracing::{Level, Subscriber}; 9 | use tracing_subscriber::filter::filter_fn; 10 | use tracing_subscriber::layer::Context; 11 | use tracing_subscriber::prelude::*; 12 | use tracing_subscriber::registry::LookupSpan; 13 | use tracing_subscriber::{EnvFilter, Layer}; 14 | 15 | /// A layer that emits pretty access logs for stellation servers. 16 | #[derive(Debug, Default)] 17 | pub struct AccessLog {} 18 | 19 | /// Returns a layer that emits pretty access logs. 20 | pub fn pretty_access() -> AccessLog { 21 | AccessLog {} 22 | } 23 | 24 | impl Layer for AccessLog 25 | where 26 | S: Subscriber + for<'lookup> LookupSpan<'lookup>, 27 | { 28 | fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) { 29 | if event.metadata().target() != "stellation_backend::endpoint::trace" { 30 | return; 31 | } 32 | 33 | #[derive(Default, Debug)] 34 | struct Values { 35 | duration: Option, 36 | path: Option, 37 | method: Option, 38 | status: Option, 39 | } 40 | 41 | impl Visit for Values { 42 | fn record_u128(&mut self, field: &tracing::field::Field, value: u128) { 43 | if field.as_ref() == "duration" { 44 | self.duration = Some(value); 45 | } 46 | } 47 | 48 | fn record_str(&mut self, field: &tracing::field::Field, value: &str) { 49 | if field.as_ref() == "path" { 50 | self.path = Some(value.to_string()); 51 | } 52 | } 53 | 54 | fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { 55 | if field.as_ref() == "status" { 56 | self.status = Some(value); 57 | } 58 | } 59 | 60 | fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { 61 | if field.as_ref() == "method" { 62 | self.method = Some(format!("{value:?}")); 63 | } 64 | } 65 | } 66 | 67 | let mut values = Values::default(); 68 | event.record(&mut values); 69 | 70 | if let (Some(path), Some(duration), Some(status), Some(method)) = 71 | (values.path, values.duration, values.status, values.method) 72 | { 73 | let duration = Some(duration) 74 | .and_then(|m| i32::try_from(m).ok()) 75 | .map(f64::from) 76 | .expect("duration took too long") 77 | / 100_000.0; 78 | 79 | let status = match status { 80 | m if m < 200 => style(m).cyan(), 81 | m if m < 300 => style(m).green(), 82 | m if m < 400 => style(m).yellow(), 83 | m => style(m).red(), 84 | } 85 | .bold(); 86 | 87 | eprintln!("{method:>6} {status} {duration:>8.2}ms {path}"); 88 | } 89 | } 90 | } 91 | 92 | /// Initialise tracing with default settings. 93 | pub fn init_default(var_name: S) 94 | where 95 | S: Into, 96 | { 97 | let var_name = var_name.into(); 98 | let env_filter = EnvFilter::builder() 99 | .with_default_directive(Level::INFO.into()) 100 | .with_env_var(var_name) 101 | .from_env_lossy(); 102 | 103 | match env::var(StctlMetadata::ENV_NAME) { 104 | Ok(_) => { 105 | // Register pretty logging if under development server. 106 | tracing_subscriber::registry() 107 | .with(pretty_access()) 108 | .with( 109 | tracing_subscriber::fmt::layer() 110 | .compact() 111 | // access logs are processed by the access log layer 112 | .with_filter(filter_fn(|metadata| { 113 | metadata.target() != "stellation_backend::endpoint::trace" 114 | })), 115 | ) 116 | .with(env_filter) 117 | .init(); 118 | } 119 | Err(_) => { 120 | tracing_subscriber::registry() 121 | .with(tracing_subscriber::fmt::layer().compact()) 122 | .with(env_filter) 123 | .init(); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /crates/stellation-backend-tower/src/server.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::future::Future; 3 | use std::net::SocketAddr; 4 | 5 | use futures::TryStream; 6 | use hyper::body::HttpBody; 7 | use hyper::server::accept::Accept; 8 | use hyper::server::conn::AddrIncoming; 9 | use hyper::{Body, Request, Response}; 10 | use tokio::io::{AsyncRead, AsyncWrite}; 11 | use tower::Service; 12 | use yew::platform::Runtime; 13 | 14 | // An executor to process requests on the Yew runtime. 15 | // 16 | // By spawning requests on the Yew runtime, 17 | // it processes request on the same thread as the rendering task. 18 | // 19 | // This increases performance in some environments (e.g.: in VM). 20 | #[derive(Clone, Default)] 21 | struct Executor { 22 | inner: Runtime, 23 | } 24 | 25 | impl hyper::rt::Executor for Executor 26 | where 27 | F: Future + Send + 'static, 28 | { 29 | fn execute(&self, fut: F) { 30 | self.inner.spawn_pinned(move || async move { 31 | fut.await; 32 | }); 33 | } 34 | } 35 | 36 | /// The stellation backend server. 37 | /// 38 | /// This server is a wrapper of [hyper::server::Server] that runs the request on Yew runtime. 39 | #[derive(Debug)] 40 | pub struct Server { 41 | inner: hyper::server::Builder, 42 | rt: Option, 43 | } 44 | 45 | impl Server { 46 | /// Binds an address. 47 | pub fn bind(addr: impl Into + 'static) -> Server { 48 | Server { 49 | inner: hyper::server::Server::bind(&addr.into()), 50 | rt: None, 51 | } 52 | } 53 | 54 | /// Reads connections from a stream. 55 | pub fn from_stream(stream: S) -> Server> 56 | where 57 | S: TryStream> + Send, 58 | T: AsyncRead + AsyncWrite + Send + 'static + Unpin, 59 | E: Into>, 60 | { 61 | Server { 62 | inner: hyper::server::Server::builder(hyper::server::accept::from_stream(stream)), 63 | rt: None, 64 | } 65 | } 66 | } 67 | impl Server 68 | where 69 | I: Accept, 70 | I::Error: Into>, 71 | I::Conn: AsyncRead + AsyncWrite + Unpin + Send + 'static, 72 | { 73 | /// Serves an http service that processes requests and returns responses. 74 | /// 75 | /// Services can be created by a [`warp::service`]. 76 | pub async fn serve_service(self, svc: HS) -> hyper::Result<()> 77 | where 78 | HS: Service, Response = Response, Future = HF, Error = HE> 79 | + Send 80 | + Clone 81 | + 'static, 82 | HE: Into>, 83 | HF: Future, HE>> + Send + 'static, 84 | 85 | B: HttpBody + Send + 'static, 86 | BD: Send + 'static, 87 | BE: Into>, 88 | { 89 | let make_svc = hyper::service::make_service_fn(move |_| { 90 | let svc = svc.clone(); 91 | async move { Ok::<_, Infallible>(svc.clone()) } 92 | }); 93 | 94 | self.serve_make_service(make_svc).await 95 | } 96 | 97 | /// Serves a service that creates http services. 98 | /// 99 | /// Make Services can be created by calling `into_make_service()` on an axum router. 100 | pub async fn serve_make_service( 101 | self, 102 | make_svc: MS, 103 | ) -> hyper::Result<()> 104 | where 105 | MS: for<'a> Service<&'a I::Conn, Response = HS, Error = ME, Future = MF>, 106 | ME: Into>, 107 | MF: Future> + Send + 'static, 108 | 109 | HS: Service, Response = Response, Future = HF, Error = HE> 110 | + Send 111 | + 'static, 112 | HE: Into>, 113 | HF: Future, HE>> + Send + 'static, 114 | 115 | B: HttpBody + Send + 'static, 116 | BD: Send + 'static, 117 | BE: Into>, 118 | { 119 | let Self { inner, rt } = self; 120 | 121 | inner 122 | .executor(Executor { 123 | inner: rt.unwrap_or_default(), 124 | }) 125 | .serve(make_svc) 126 | .await 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/registry/resolver.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::sync::Arc; 3 | 4 | use futures::future::{self, LocalBoxFuture}; 5 | use futures::{FutureExt, TryFutureExt}; 6 | 7 | use super::Incoming; 8 | use crate::resolvers::{MutationResolver, QueryResolver}; 9 | use crate::{BridgeError, BridgeResult}; 10 | 11 | pub(super) type Resolver = 12 | Arc, &[u8]) -> LocalBoxFuture<'static, BridgeResult>>>; 13 | 14 | pub(super) type Resolvers = Vec>; 15 | 16 | /// The Registry Builder for Resolver Registry 17 | pub struct ResolverRegistryBuilder { 18 | resolvers: Resolvers, 19 | } 20 | 21 | impl fmt::Debug for ResolverRegistryBuilder { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | f.debug_struct("ResolverRegistryBuilder") 24 | .finish_non_exhaustive() 25 | } 26 | } 27 | 28 | impl Default for ResolverRegistryBuilder { 29 | fn default() -> Self { 30 | Self { 31 | resolvers: Vec::new(), 32 | } 33 | } 34 | } 35 | 36 | impl ResolverRegistryBuilder 37 | where 38 | CTX: 'static, 39 | { 40 | /// Creates a registry builder. 41 | pub fn new() -> Self { 42 | Self::default() 43 | } 44 | 45 | /// Creates a registry. 46 | pub fn build(self) -> ResolverRegistry { 47 | ResolverRegistry { 48 | inner: Arc::new(self), 49 | } 50 | } 51 | 52 | /// Adds a Query Resolver 53 | pub fn add_query(mut self) -> Self 54 | where 55 | T: 'static + QueryResolver, 56 | { 57 | let resolver = Arc::new(|ctx: &Arc, input: &[u8]| { 58 | let ctx = ctx.clone(); 59 | let input = match bincode::deserialize::(input) 60 | .map_err(BridgeError::Encoding) 61 | .map_err(future::err) 62 | .map_err(|e| e.boxed_local()) 63 | { 64 | Ok(m) => m, 65 | Err(e) => return e, 66 | }; 67 | async move { T::resolve(&ctx, &input).await } 68 | .map(|m| bincode::serialize(&m.as_deref())) 69 | .map_err(BridgeError::Encoding) 70 | .boxed_local() 71 | }); 72 | 73 | self.resolvers.push(resolver); 74 | self 75 | } 76 | 77 | /// Adds a Mutation Resolver 78 | pub fn add_mutation(mut self) -> Self 79 | where 80 | T: 'static + MutationResolver, 81 | { 82 | let resolver = Arc::new(|ctx: &Arc, input: &[u8]| { 83 | let ctx = ctx.clone(); 84 | let input = match bincode::deserialize::(input) 85 | .map_err(BridgeError::Encoding) 86 | .map_err(future::err) 87 | .map_err(|e| e.boxed_local()) 88 | { 89 | Ok(m) => m, 90 | Err(e) => return e, 91 | }; 92 | async move { T::resolve(&ctx, &input).await } 93 | .map(|m| bincode::serialize(&m.as_deref())) 94 | .map_err(BridgeError::Encoding) 95 | .boxed_local() 96 | }); 97 | 98 | self.resolvers.push(resolver); 99 | self 100 | } 101 | } 102 | 103 | /// The Registry that holds available query and mutation resolvers. 104 | pub struct ResolverRegistry { 105 | inner: Arc>, 106 | } 107 | 108 | impl fmt::Debug for ResolverRegistry { 109 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 110 | f.debug_struct("ResolverRegistry").finish_non_exhaustive() 111 | } 112 | } 113 | 114 | impl Clone for ResolverRegistry { 115 | fn clone(&self) -> Self { 116 | Self { 117 | inner: self.inner.clone(), 118 | } 119 | } 120 | } 121 | 122 | impl PartialEq for ResolverRegistry { 123 | fn eq(&self, other: &Self) -> bool { 124 | Arc::ptr_eq(&self.inner, &other.inner) 125 | } 126 | } 127 | 128 | impl Eq for ResolverRegistry {} 129 | 130 | impl ResolverRegistry { 131 | /// Creates a Builder for remote registry. 132 | pub fn builder() -> ResolverRegistryBuilder { 133 | ResolverRegistryBuilder::new() 134 | } 135 | 136 | /// Resolves an encoded request. 137 | pub async fn resolve_encoded(&self, ctx: &Arc, incoming: &[u8]) -> BridgeResult> { 138 | let incoming: Incoming<'_> = bincode::deserialize(incoming)?; 139 | 140 | let resolver = self 141 | .inner 142 | .resolvers 143 | .get(incoming.query_index) 144 | .ok_or(BridgeError::InvalidIndex(incoming.query_index))?; 145 | 146 | resolver(ctx, incoming.input).await 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/hooks/use_bridged_query.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::ops::Deref; 3 | use std::rc::Rc; 4 | 5 | use bounce::query::{use_prepared_query, QueryState, UseQueryHandle}; 6 | use yew::prelude::*; 7 | use yew::suspense::SuspensionResult; 8 | 9 | use super::use_bridged_query_value::BridgedQueryInner; 10 | use crate::links::Link; 11 | use crate::routines::{BridgedQuery, QueryResult}; 12 | 13 | /// Bridged Query State 14 | #[derive(Debug, PartialEq)] 15 | pub enum BridgedQueryState 16 | where 17 | T: BridgedQuery + 'static, 18 | { 19 | /// The query has completed. 20 | Completed { 21 | /// Result of the completed query. 22 | result: QueryResult, 23 | }, 24 | /// A previous query has completed and a new query is currently loading. 25 | Refreshing { 26 | /// Result of last completed query. 27 | last_result: QueryResult, 28 | }, 29 | } 30 | 31 | impl Clone for BridgedQueryState 32 | where 33 | T: BridgedQuery + 'static, 34 | { 35 | fn clone(&self) -> Self { 36 | match self { 37 | Self::Completed { result } => Self::Completed { 38 | result: result.clone(), 39 | }, 40 | Self::Refreshing { last_result } => Self::Refreshing { 41 | last_result: last_result.clone(), 42 | }, 43 | } 44 | } 45 | } 46 | 47 | impl PartialEq<&BridgedQueryState> for BridgedQueryState 48 | where 49 | T: BridgedQuery + 'static, 50 | { 51 | fn eq(&self, other: &&BridgedQueryState) -> bool { 52 | self == *other 53 | } 54 | } 55 | 56 | impl PartialEq> for &'_ BridgedQueryState 57 | where 58 | T: BridgedQuery + 'static, 59 | { 60 | fn eq(&self, other: &BridgedQueryState) -> bool { 61 | *self == other 62 | } 63 | } 64 | 65 | /// A handle returned by [`use_bridged_query`]. 66 | /// 67 | /// This type dereferences to [`QueryResult`]. 68 | pub struct UseBridgedQueryHandle 69 | where 70 | T: BridgedQuery + 'static, 71 | L: 'static + Link, 72 | { 73 | inner: UseQueryHandle>, 74 | state: Rc>, 75 | } 76 | 77 | impl UseBridgedQueryHandle 78 | where 79 | T: BridgedQuery + 'static, 80 | L: 'static + Link, 81 | { 82 | /// Returns the state of current query. 83 | pub fn state(&self) -> &BridgedQueryState { 84 | self.state.as_ref() 85 | } 86 | 87 | /// Refreshes the query. 88 | /// 89 | /// The query will be refreshed with the input provided to the hook. 90 | pub async fn refresh(&self) -> QueryResult { 91 | self.inner.refresh().await?.inner.clone() 92 | } 93 | } 94 | 95 | impl Clone for UseBridgedQueryHandle 96 | where 97 | T: BridgedQuery + 'static, 98 | L: 'static + Link, 99 | { 100 | fn clone(&self) -> Self { 101 | Self { 102 | inner: self.inner.clone(), 103 | state: self.state.clone(), 104 | } 105 | } 106 | } 107 | 108 | impl Deref for UseBridgedQueryHandle 109 | where 110 | T: BridgedQuery + 'static, 111 | L: 'static + Link, 112 | { 113 | type Target = QueryResult; 114 | 115 | fn deref(&self) -> &Self::Target { 116 | match self.state() { 117 | BridgedQueryState::Completed { result } 118 | | BridgedQueryState::Refreshing { 119 | last_result: result, 120 | } => result, 121 | } 122 | } 123 | } 124 | 125 | impl fmt::Debug for UseBridgedQueryHandle 126 | where 127 | T: BridgedQuery + fmt::Debug + 'static, 128 | L: 'static + Link, 129 | { 130 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 131 | f.debug_struct("UseBridgedQueryHandle") 132 | .field("state", self.state()) 133 | .finish() 134 | } 135 | } 136 | 137 | /// Bridges a query. 138 | #[hook] 139 | pub fn use_bridged_query(input: Rc) -> SuspensionResult> 140 | where 141 | Q: 'static + BridgedQuery, 142 | L: 'static + Link, 143 | { 144 | let handle = use_prepared_query::>(input)?; 145 | let state = use_memo( 146 | |state| match state { 147 | QueryState::Completed { result } => BridgedQueryState::Completed { 148 | result: result 149 | .as_ref() 150 | .map_err(|e| e.clone()) 151 | .and_then(|m| m.inner.clone()), 152 | }, 153 | QueryState::Refreshing { last_result } => BridgedQueryState::Refreshing { 154 | last_result: last_result 155 | .as_ref() 156 | .map_err(|e| e.clone()) 157 | .and_then(|m| m.inner.clone()), 158 | }, 159 | }, 160 | handle.state().clone(), 161 | ); 162 | 163 | Ok(UseBridgedQueryHandle { 164 | inner: handle, 165 | state, 166 | }) 167 | } 168 | -------------------------------------------------------------------------------- /examples/fullstack/view/src/pages/greeting.rs: -------------------------------------------------------------------------------- 1 | use example_fullstack_api::{Bridge, GreetingMutation}; 2 | use stylist::yew::styled_component; 3 | use web_sys::HtmlInputElement; 4 | use yew::platform::spawn_local; 5 | use yew::prelude::*; 6 | 7 | #[styled_component] 8 | pub fn Greeting() -> Html { 9 | let handle = Bridge::use_mutation::(); 10 | 11 | let message = match handle.result() { 12 | None => "".to_string(), 13 | Some(Ok(m)) => m.message.to_string(), 14 | Some(Err(_)) => "failed to communicate with server...".into(), 15 | }; 16 | 17 | let input_ref = use_node_ref(); 18 | 19 | let name = use_state_eq(|| "".to_string()); 20 | let oninput = use_callback( 21 | |input: InputEvent, set_value| { 22 | let el = input.target_unchecked_into::(); 23 | set_value.set(el.value()); 24 | }, 25 | name.setter(), 26 | ); 27 | 28 | let onclick = { 29 | let input_ref = input_ref.clone(); 30 | 31 | use_callback( 32 | move |_input, name| { 33 | if !input_ref 34 | .cast::() 35 | .map(|m| m.report_validity()) 36 | .unwrap_or(false) 37 | { 38 | return; 39 | } 40 | 41 | let name = name.clone(); 42 | let handle = handle.clone(); 43 | spawn_local(async move { 44 | let _ = handle.run(name.to_string()).await; 45 | }); 46 | }, 47 | name.clone(), 48 | ) 49 | }; 50 | 51 | html! { 52 |

57 |
58 | 106 | 134 |
135 |
{message}
141 |
142 | } 143 | } 144 | -------------------------------------------------------------------------------- /templates/default/crates/view/src/pages/greeting.rs: -------------------------------------------------------------------------------- 1 | use stylist::yew::styled_component; 2 | use web_sys::HtmlInputElement; 3 | use yew::platform::spawn_local; 4 | use yew::prelude::*; 5 | 6 | use crate::api::{Bridge, GreetingMutation}; 7 | 8 | #[styled_component] 9 | pub fn Greeting() -> Html { 10 | let handle = Bridge::use_mutation::(); 11 | 12 | let message = match handle.result() { 13 | None => "".to_string(), 14 | Some(Ok(m)) => m.message.to_string(), 15 | Some(Err(_)) => "failed to communicate with server...".into(), 16 | }; 17 | 18 | let input_ref = use_node_ref(); 19 | 20 | let name = use_state_eq(|| "".to_string()); 21 | let oninput = use_callback( 22 | |input: InputEvent, set_value| { 23 | let el = input.target_unchecked_into::(); 24 | set_value.set(el.value()); 25 | }, 26 | name.setter(), 27 | ); 28 | 29 | let onclick = { 30 | let input_ref = input_ref.clone(); 31 | 32 | use_callback( 33 | move |_input, name| { 34 | if !input_ref 35 | .cast::() 36 | .map(|m| m.report_validity()) 37 | .unwrap_or(false) 38 | { 39 | return; 40 | } 41 | 42 | let name = name.clone(); 43 | let handle = handle.clone(); 44 | spawn_local(async move { 45 | let _ = handle.run(name.to_string()).await; 46 | }); 47 | }, 48 | name.clone(), 49 | ) 50 | }; 51 | 52 | html! { 53 |
58 |
59 | 107 | 135 |
136 |
{message}
142 |
143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/registry/routine.rs: -------------------------------------------------------------------------------- 1 | use std::any::TypeId; 2 | use std::fmt; 3 | use std::rc::Rc; 4 | use std::sync::Arc; 5 | 6 | use super::Incoming; 7 | use crate::routines::{BridgedMutation, BridgedQuery, MutationResult, QueryResult}; 8 | use crate::{BridgeError, BridgeResult}; 9 | 10 | /// The Registry Builder for Routine Registry 11 | #[derive(Default)] 12 | pub struct RoutineRegistryBuilder { 13 | query_ids: Vec, 14 | } 15 | 16 | impl fmt::Debug for RoutineRegistryBuilder { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | f.debug_struct("RoutineRegistryBuilder") 19 | .finish_non_exhaustive() 20 | } 21 | } 22 | 23 | impl RoutineRegistryBuilder { 24 | /// Creates a registry builder. 25 | pub fn new() -> Self { 26 | Self::default() 27 | } 28 | 29 | /// Creates a registry. 30 | pub fn build(self) -> RoutineRegistry { 31 | RoutineRegistry { 32 | inner: Arc::new(self), 33 | } 34 | } 35 | 36 | /// Adds a mutation. 37 | pub fn add_mutation(mut self) -> Self 38 | where 39 | T: 'static + BridgedMutation, 40 | { 41 | let type_id = TypeId::of::(); 42 | self.query_ids.push(type_id); 43 | 44 | self 45 | } 46 | 47 | /// Adds a query. 48 | pub fn add_query(mut self) -> Self 49 | where 50 | T: 'static + BridgedQuery, 51 | { 52 | let type_id = TypeId::of::(); 53 | self.query_ids.push(type_id); 54 | 55 | self 56 | } 57 | } 58 | 59 | /// The Registry that holds available queries and mutations. 60 | pub struct RoutineRegistry { 61 | inner: Arc, 62 | } 63 | 64 | impl fmt::Debug for RoutineRegistry { 65 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 66 | f.debug_struct("RoutineRegistry").finish_non_exhaustive() 67 | } 68 | } 69 | 70 | impl PartialEq for RoutineRegistry { 71 | fn eq(&self, other: &Self) -> bool { 72 | Arc::ptr_eq(&self.inner, &other.inner) 73 | } 74 | } 75 | 76 | impl Eq for RoutineRegistry {} 77 | 78 | impl Clone for RoutineRegistry { 79 | fn clone(&self) -> Self { 80 | Self { 81 | inner: self.inner.clone(), 82 | } 83 | } 84 | } 85 | 86 | impl RoutineRegistry { 87 | /// Creates a Builder for remote registry. 88 | pub fn builder() -> RoutineRegistryBuilder { 89 | RoutineRegistryBuilder::new() 90 | } 91 | 92 | /// The method to encode the query input for a remote link. 93 | pub(crate) fn encode_query_input(&self, input: &T::Input) -> BridgeResult> 94 | where 95 | T: 'static + BridgedQuery, 96 | { 97 | let input = bincode::serialize(&input).map_err(BridgeError::Encoding)?; 98 | let type_id = TypeId::of::(); 99 | 100 | let query_index = self 101 | .inner 102 | .query_ids 103 | .iter() 104 | .enumerate() 105 | .find(|(_, m)| **m == type_id) 106 | .ok_or(BridgeError::InvalidType(type_id))? 107 | .0; 108 | 109 | let incoming = Incoming { 110 | query_index, 111 | input: &input, 112 | }; 113 | 114 | bincode::serialize(&incoming).map_err(BridgeError::Encoding) 115 | } 116 | 117 | /// The method to decode the query output for a remote link. 118 | pub(crate) fn decode_query_output(&self, output: &[u8]) -> QueryResult 119 | where 120 | T: 'static + BridgedQuery, 121 | { 122 | bincode::deserialize::>(output) 123 | .map_err(BridgeError::Encoding) 124 | .map_err(T::into_query_error)? 125 | .map(Rc::new) 126 | } 127 | 128 | /// The method to encode the mutation input for a remote link. 129 | pub(crate) fn encode_mutation_input(&self, input: &T::Input) -> BridgeResult> 130 | where 131 | T: 'static + BridgedMutation, 132 | { 133 | let input = bincode::serialize(&input).map_err(BridgeError::Encoding)?; 134 | let type_id = TypeId::of::(); 135 | 136 | let query_index = self 137 | .inner 138 | .query_ids 139 | .iter() 140 | .enumerate() 141 | .find(|(_, m)| **m == type_id) 142 | .ok_or(BridgeError::InvalidType(type_id))? 143 | .0; 144 | 145 | let incoming = Incoming { 146 | query_index, 147 | input: &input, 148 | }; 149 | 150 | bincode::serialize(&incoming).map_err(BridgeError::Encoding) 151 | } 152 | 153 | /// The method to decode the mutation output for a remote link. 154 | pub(crate) fn decode_mutation_output(&self, output: &[u8]) -> MutationResult 155 | where 156 | T: 'static + BridgedMutation, 157 | { 158 | bincode::deserialize::>(output) 159 | .map_err(BridgeError::Encoding) 160 | .map_err(T::into_mutation_error)? 161 | .map(Rc::new) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/hooks/use_bridged_mutation.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::marker::PhantomData; 3 | use std::rc::Rc; 4 | 5 | use async_trait::async_trait; 6 | use bounce::query::{use_mutation, MutationState, UseMutationHandle}; 7 | use bounce::BounceStates; 8 | use yew::prelude::*; 9 | 10 | use crate::links::Link; 11 | use crate::routines::{BridgedMutation, MutationResult}; 12 | use crate::state::BridgeSelector; 13 | 14 | /// Bridged Mutation State 15 | #[derive(Debug, PartialEq)] 16 | pub enum BridgedMutationState 17 | where 18 | T: BridgedMutation + 'static, 19 | { 20 | /// The mutation has not started yet. 21 | Idle, 22 | /// The mutation is loading. 23 | Loading, 24 | /// The mutation has completed. 25 | Completed { 26 | /// Result of the completed mutation. 27 | result: MutationResult, 28 | }, 29 | /// A previous mutation has completed and a new mutation is currently loading. 30 | Refreshing { 31 | /// Result of last completed mutation. 32 | last_result: MutationResult, 33 | }, 34 | } 35 | 36 | impl Clone for BridgedMutationState 37 | where 38 | T: BridgedMutation + 'static, 39 | { 40 | fn clone(&self) -> Self { 41 | match self { 42 | Self::Idle => Self::Idle, 43 | Self::Loading => Self::Loading, 44 | Self::Completed { result } => Self::Completed { 45 | result: result.clone(), 46 | }, 47 | Self::Refreshing { last_result } => Self::Refreshing { 48 | last_result: last_result.clone(), 49 | }, 50 | } 51 | } 52 | } 53 | 54 | impl PartialEq<&BridgedMutationState> for BridgedMutationState 55 | where 56 | T: BridgedMutation + 'static, 57 | { 58 | fn eq(&self, other: &&BridgedMutationState) -> bool { 59 | self == *other 60 | } 61 | } 62 | 63 | impl PartialEq> for &'_ BridgedMutationState 64 | where 65 | T: BridgedMutation + 'static, 66 | { 67 | fn eq(&self, other: &BridgedMutationState) -> bool { 68 | *self == other 69 | } 70 | } 71 | 72 | struct BridgedMutationInner 73 | where 74 | M: BridgedMutation, 75 | { 76 | inner: MutationResult, 77 | _marker: PhantomData, 78 | } 79 | 80 | impl PartialEq for BridgedMutationInner 81 | where 82 | M: BridgedMutation, 83 | { 84 | fn eq(&self, other: &Self) -> bool { 85 | self.inner == other.inner 86 | } 87 | } 88 | 89 | #[async_trait(?Send)] 90 | impl bounce::query::Mutation for BridgedMutationInner 91 | where 92 | M: 'static + BridgedMutation, 93 | L: 'static + Link, 94 | { 95 | type Error = M::Error; 96 | type Input = M::Input; 97 | 98 | async fn run( 99 | states: &BounceStates, 100 | input: Rc, 101 | ) -> bounce::query::MutationResult { 102 | let bridge = states.get_selector_value::>(); 103 | let link = bridge.link(); 104 | 105 | Ok(Self { 106 | inner: link.resolve_mutation::(&input).await, 107 | _marker: PhantomData, 108 | } 109 | .into()) 110 | } 111 | } 112 | 113 | /// A handle returned by [`use_bridged_mutation`]. 114 | /// 115 | /// This can be used to access the result or start the mutation. 116 | pub struct UseBridgedMutationHandle 117 | where 118 | T: BridgedMutation + 'static, 119 | L: 'static + Link, 120 | { 121 | inner: UseMutationHandle>, 122 | state: Rc>, 123 | } 124 | 125 | impl UseBridgedMutationHandle 126 | where 127 | T: BridgedMutation + 'static, 128 | L: 'static + Link, 129 | { 130 | /// Runs a mutation with input. 131 | pub async fn run(&self, input: impl Into>) -> MutationResult { 132 | self.inner.run(input).await?.inner.clone() 133 | } 134 | 135 | /// Returns the state of current mutation. 136 | pub fn state(&self) -> &BridgedMutationState { 137 | self.state.as_ref() 138 | } 139 | 140 | /// Returns the result of last finished mutation (if any). 141 | /// 142 | /// - `None` indicates that a mutation is currently loading or has yet to start(idling). 143 | /// - `Some(Ok(m))` indicates that the last mutation is successful and the content is stored in 144 | /// `m`. 145 | /// - `Some(Err(e))` indicates that the last mutation has failed and the error is stored in `e`. 146 | pub fn result(&self) -> Option<&MutationResult> { 147 | match self.state() { 148 | BridgedMutationState::Idle | BridgedMutationState::Loading => None, 149 | BridgedMutationState::Completed { result } 150 | | BridgedMutationState::Refreshing { 151 | last_result: result, 152 | } => Some(result), 153 | } 154 | } 155 | } 156 | 157 | impl fmt::Debug for UseBridgedMutationHandle 158 | where 159 | T: BridgedMutation + fmt::Debug + 'static, 160 | L: 'static + Link, 161 | { 162 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 163 | f.debug_struct("UseBridgedMutationHandle") 164 | .field("state", &self.state()) 165 | .finish() 166 | } 167 | } 168 | 169 | impl Clone for UseBridgedMutationHandle 170 | where 171 | T: BridgedMutation + 'static, 172 | L: 'static + Link, 173 | { 174 | fn clone(&self) -> Self { 175 | Self { 176 | inner: self.inner.clone(), 177 | state: self.state.clone(), 178 | } 179 | } 180 | } 181 | 182 | /// Bridges a mutation. 183 | #[hook] 184 | pub fn use_bridged_mutation() -> UseBridgedMutationHandle 185 | where 186 | T: 'static + BridgedMutation, 187 | L: 'static + Link, 188 | { 189 | let handle = use_mutation::>(); 190 | let state = use_memo( 191 | |state| match state { 192 | MutationState::Idle => BridgedMutationState::Idle, 193 | MutationState::Loading => BridgedMutationState::Loading, 194 | MutationState::Completed { result } => BridgedMutationState::Completed { 195 | result: result 196 | .as_ref() 197 | .map_err(|e| e.clone()) 198 | .and_then(|m| m.inner.clone()), 199 | }, 200 | MutationState::Refreshing { last_result } => BridgedMutationState::Refreshing { 201 | last_result: last_result 202 | .as_ref() 203 | .map_err(|e| e.clone()) 204 | .and_then(|m| m.inner.clone()), 205 | }, 206 | }, 207 | handle.state().clone(), 208 | ); 209 | 210 | UseBridgedMutationHandle { 211 | inner: handle, 212 | state, 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /crates/stctl/src/paths.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use anyhow::{Context, Result}; 4 | use tokio::fs; 5 | use tokio::sync::OnceCell; 6 | 7 | #[derive(Debug, Clone)] 8 | pub(crate) struct Paths { 9 | workspace_dir: PathBuf, 10 | build_dir: OnceCell, 11 | data_dir: OnceCell, 12 | frontend_data_dir: OnceCell, 13 | backend_data_dir: OnceCell, 14 | 15 | frontend_builds_dir: OnceCell, 16 | backend_builds_dir: OnceCell, 17 | 18 | frontend_logs_dir: OnceCell, 19 | backend_logs_dir: OnceCell, 20 | } 21 | 22 | impl Paths { 23 | /// Creates a new `Paths` for stctl. 24 | pub async fn new

(manifest_path: P) -> Result 25 | where 26 | P: AsRef, 27 | { 28 | let manifest_path = manifest_path.as_ref(); 29 | 30 | let workspace_dir = manifest_path 31 | .canonicalize()? 32 | .parent() 33 | .context("failed to find workspace directory") 34 | .map(|m| m.to_owned())?; 35 | 36 | Ok(Self { 37 | workspace_dir, 38 | build_dir: OnceCell::new(), 39 | data_dir: OnceCell::new(), 40 | frontend_data_dir: OnceCell::new(), 41 | backend_data_dir: OnceCell::new(), 42 | frontend_builds_dir: OnceCell::new(), 43 | backend_builds_dir: OnceCell::new(), 44 | frontend_logs_dir: OnceCell::new(), 45 | backend_logs_dir: OnceCell::new(), 46 | }) 47 | } 48 | 49 | /// Returns the workspace directory. 50 | /// 51 | /// This is the parent directory of `stellation.toml`. 52 | /// 53 | /// # Note 54 | /// 55 | /// This can be different than the cargo workspace directory. 56 | /// 57 | /// This determines the location of `.stellation` data directory and `build` final artifact 58 | /// directory. This is subject to change in future releases. 59 | pub async fn workspace_dir(&self) -> Result<&Path> { 60 | Ok(&self.workspace_dir) 61 | } 62 | 63 | /// Creates and returns the path of the build directory. 64 | /// 65 | /// This is the `build` directory in the same parent directory as `stellation.toml`. 66 | /// 67 | /// # Note 68 | /// 69 | /// This should not be confused with the `builds` directory. 70 | pub async fn build_dir(&self) -> Result<&Path> { 71 | self.build_dir 72 | .get_or_try_init(|| async { 73 | let dir = self.workspace_dir().await?.join("build"); 74 | 75 | fs::create_dir_all(&dir) 76 | .await 77 | .context("failed to create build directory")?; 78 | 79 | Ok(dir) 80 | }) 81 | .await 82 | .map(|m| m.as_ref()) 83 | } 84 | 85 | /// Creates and returns the path of the data directory. 86 | /// 87 | /// This is the `.stellation` directory in the same parent directory as `stellation.toml`. 88 | pub async fn data_dir(&self) -> Result<&Path> { 89 | self.data_dir 90 | .get_or_try_init(|| async { 91 | let dir = self.workspace_dir().await?.join(".stellation"); 92 | 93 | fs::create_dir_all(&dir) 94 | .await 95 | .context("failed to create data directory")?; 96 | 97 | Ok(dir) 98 | }) 99 | .await 100 | .map(|m| m.as_ref()) 101 | } 102 | 103 | /// Creates and returns the path of the frontend data directory. 104 | /// 105 | /// This is the `.stellation/frontend` directory in the same parent directory as 106 | /// `stellation.toml`. 107 | pub async fn frontend_data_dir(&self) -> Result<&Path> { 108 | self.frontend_data_dir 109 | .get_or_try_init(|| async { 110 | let dir = self.data_dir().await?.join("frontend"); 111 | 112 | fs::create_dir_all(&dir) 113 | .await 114 | .context("failed to create frontend data directory")?; 115 | 116 | Ok(dir) 117 | }) 118 | .await 119 | .map(|m| m.as_ref()) 120 | } 121 | 122 | /// Creates and returns the path of the backend data directory. 123 | /// 124 | /// This is the `.stellation/backend` directory in the same parent directory as 125 | /// `stellation.toml`. 126 | pub async fn backend_data_dir(&self) -> Result<&Path> { 127 | self.backend_data_dir 128 | .get_or_try_init(|| async { 129 | let dir = self.data_dir().await?.join("backend"); 130 | 131 | fs::create_dir_all(&dir) 132 | .await 133 | .context("failed to create backend data directory")?; 134 | 135 | Ok(dir) 136 | }) 137 | .await 138 | .map(|m| m.as_ref()) 139 | } 140 | 141 | /// Creates and returns the path of the frontend builds directory. 142 | /// 143 | /// This is the `.stellation/frontend/builds` directory in the same parent directory as 144 | /// `stellation.toml`. 145 | pub async fn frontend_builds_dir(&self) -> Result<&Path> { 146 | self.frontend_builds_dir 147 | .get_or_try_init(|| async { 148 | let dir = self.frontend_data_dir().await?.join("builds"); 149 | 150 | fs::create_dir_all(&dir) 151 | .await 152 | .context("failed to create builds directory for frontend build.")?; 153 | 154 | Ok(dir) 155 | }) 156 | .await 157 | .map(|m| m.as_ref()) 158 | } 159 | 160 | /// Creates and returns the path of the backend builds directory. 161 | /// 162 | /// This is the `.stellation/backend/builds` directory in the same parent directory as 163 | /// `stellation.toml`. 164 | pub async fn backend_builds_dir(&self) -> Result<&Path> { 165 | self.backend_builds_dir 166 | .get_or_try_init(|| async { 167 | let dir = self.backend_data_dir().await?.join("builds"); 168 | 169 | fs::create_dir_all(&dir) 170 | .await 171 | .context("failed to create builds directory for backend build.")?; 172 | 173 | Ok(dir) 174 | }) 175 | .await 176 | .map(|m| m.as_ref()) 177 | } 178 | 179 | /// Creates and returns the path of the frontend logs directory. 180 | /// 181 | /// This is the `.stellation/frontend/logs` directory in the same parent directory as 182 | /// `stellation.toml`. 183 | pub async fn frontend_logs_dir(&self) -> Result<&Path> { 184 | self.frontend_logs_dir 185 | .get_or_try_init(|| async { 186 | let dir = self.frontend_data_dir().await?.join("logs"); 187 | 188 | fs::create_dir_all(&dir) 189 | .await 190 | .context("failed to create logs directory for frontend build.")?; 191 | 192 | Ok(dir) 193 | }) 194 | .await 195 | .map(|m| m.as_ref()) 196 | } 197 | 198 | /// Creates and returns the path of the backend logs directory. 199 | /// 200 | /// This is the `.stellation/backend/logs` directory in the same parent directory as 201 | /// `stellation.toml`. 202 | pub async fn backend_logs_dir(&self) -> Result<&Path> { 203 | self.backend_logs_dir 204 | .get_or_try_init(|| async { 205 | let dir = self.backend_data_dir().await?.join("logs"); 206 | 207 | fs::create_dir_all(&dir) 208 | .await 209 | .context("failed to create logs directory for backend build.")?; 210 | 211 | Ok(dir) 212 | }) 213 | .await 214 | .map(|m| m.as_ref()) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /crates/stellation-bridge/src/hooks/use_bridged_query_value.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::marker::PhantomData; 3 | use std::rc::Rc; 4 | 5 | use async_trait::async_trait; 6 | use bounce::query::{use_query_value, QueryValueState, UseQueryValueHandle}; 7 | use bounce::BounceStates; 8 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 9 | use yew::prelude::*; 10 | 11 | use crate::links::Link; 12 | use crate::routines::{BridgedQuery, QueryResult}; 13 | use crate::state::BridgeSelector; 14 | 15 | /// Bridged Query Value State 16 | #[derive(Debug, PartialEq)] 17 | pub enum BridgedQueryValueState 18 | where 19 | T: BridgedQuery + 'static, 20 | { 21 | /// The query is loading. 22 | Loading, 23 | /// The query has completed. 24 | Completed { 25 | /// Result of the completed query. 26 | result: QueryResult, 27 | }, 28 | /// A previous query has completed and a new query is currently loading. 29 | Refreshing { 30 | /// Result of last completed query. 31 | last_result: QueryResult, 32 | }, 33 | } 34 | 35 | impl Clone for BridgedQueryValueState 36 | where 37 | T: BridgedQuery + 'static, 38 | { 39 | fn clone(&self) -> Self { 40 | match self { 41 | Self::Loading => Self::Loading, 42 | Self::Completed { result } => Self::Completed { 43 | result: result.clone(), 44 | }, 45 | Self::Refreshing { last_result } => Self::Refreshing { 46 | last_result: last_result.clone(), 47 | }, 48 | } 49 | } 50 | } 51 | 52 | impl PartialEq<&BridgedQueryValueState> for BridgedQueryValueState 53 | where 54 | T: BridgedQuery + 'static, 55 | { 56 | fn eq(&self, other: &&BridgedQueryValueState) -> bool { 57 | self == *other 58 | } 59 | } 60 | 61 | impl PartialEq> for &'_ BridgedQueryValueState 62 | where 63 | T: BridgedQuery + 'static, 64 | { 65 | fn eq(&self, other: &BridgedQueryValueState) -> bool { 66 | *self == other 67 | } 68 | } 69 | 70 | #[derive(Debug)] 71 | pub(super) struct BridgedQueryInner 72 | where 73 | Q: BridgedQuery, 74 | { 75 | pub inner: QueryResult, 76 | _marker: PhantomData, 77 | } 78 | 79 | impl Clone for BridgedQueryInner 80 | where 81 | Q: BridgedQuery, 82 | { 83 | fn clone(&self) -> Self { 84 | Self { 85 | inner: self.inner.clone(), 86 | _marker: PhantomData, 87 | } 88 | } 89 | } 90 | 91 | impl PartialEq for BridgedQueryInner 92 | where 93 | Q: BridgedQuery + PartialEq, 94 | { 95 | fn eq(&self, other: &Self) -> bool { 96 | self.inner == other.inner 97 | } 98 | } 99 | impl Eq for BridgedQueryInner where Q: BridgedQuery + Eq {} 100 | 101 | impl Serialize for BridgedQueryInner 102 | where 103 | Q: BridgedQuery, 104 | { 105 | fn serialize(&self, serializer: S) -> Result 106 | where 107 | S: Serializer, 108 | { 109 | self.inner.as_deref().serialize(serializer) 110 | } 111 | } 112 | 113 | impl<'de, Q, L> Deserialize<'de> for BridgedQueryInner 114 | where 115 | Q: BridgedQuery, 116 | { 117 | fn deserialize(deserializer: D) -> Result 118 | where 119 | D: Deserializer<'de>, 120 | { 121 | Ok(Self { 122 | inner: std::result::Result::::deserialize(deserializer)?.map(Rc::new), 123 | _marker: PhantomData, 124 | }) 125 | } 126 | } 127 | #[async_trait(?Send)] 128 | impl bounce::query::Query for BridgedQueryInner 129 | where 130 | Q: 'static + BridgedQuery, 131 | L: 'static + Link, 132 | { 133 | type Error = Q::Error; 134 | type Input = Q::Input; 135 | 136 | async fn query( 137 | states: &BounceStates, 138 | input: Rc, 139 | ) -> bounce::query::QueryResult { 140 | let bridge = states.get_selector_value::>(); 141 | let link = bridge.link(); 142 | 143 | Ok(Self { 144 | inner: link.resolve_query::(&input).await, 145 | _marker: PhantomData, 146 | } 147 | .into()) 148 | } 149 | } 150 | 151 | /// A handle returned by [`use_bridged_query_value`]. 152 | pub struct UseBridgedQueryValueHandle 153 | where 154 | T: BridgedQuery + 'static, 155 | L: 'static + Link, 156 | { 157 | inner: UseQueryValueHandle>, 158 | state: Rc>, 159 | } 160 | 161 | impl UseBridgedQueryValueHandle 162 | where 163 | T: BridgedQuery + 'static, 164 | L: 'static + Link, 165 | { 166 | /// Returns the state of current query. 167 | pub fn state(&self) -> &BridgedQueryValueState { 168 | self.state.as_ref() 169 | } 170 | 171 | /// Returns the result of current query (if any). 172 | /// 173 | /// - `None` indicates that the query is currently loading. 174 | /// - `Some(Ok(m))` indicates that the query is successful and the content is stored in `m`. 175 | /// - `Some(Err(e))` indicates that the query has failed and the error is stored in `e`. 176 | pub fn result(&self) -> Option<&QueryResult> { 177 | match self.state() { 178 | BridgedQueryValueState::Completed { result, .. } 179 | | BridgedQueryValueState::Refreshing { 180 | last_result: result, 181 | .. 182 | } => Some(result), 183 | _ => None, 184 | } 185 | } 186 | 187 | /// Refreshes the query. 188 | /// 189 | /// The query will be refreshed with the input provided to the hook. 190 | pub async fn refresh(&self) -> QueryResult { 191 | self.inner.refresh().await?.inner.clone() 192 | } 193 | } 194 | 195 | impl Clone for UseBridgedQueryValueHandle 196 | where 197 | T: BridgedQuery + 'static, 198 | L: 'static + Link, 199 | { 200 | fn clone(&self) -> Self { 201 | Self { 202 | inner: self.inner.clone(), 203 | state: self.state.clone(), 204 | } 205 | } 206 | } 207 | 208 | impl fmt::Debug for UseBridgedQueryValueHandle 209 | where 210 | T: BridgedQuery + fmt::Debug + 'static, 211 | L: 'static + Link, 212 | { 213 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 214 | f.debug_struct("UseBridgedQueryValueHandle") 215 | .field("state", self.state()) 216 | .finish() 217 | } 218 | } 219 | 220 | /// Bridges a query as value. 221 | /// 222 | /// # Note 223 | /// 224 | /// This hook does not suspend the component and the data is not fetched during SSR. 225 | /// If this hook is used in SSR, this hook will remain as loading state. 226 | #[hook] 227 | pub fn use_bridged_query_value(input: Rc) -> UseBridgedQueryValueHandle 228 | where 229 | Q: 'static + BridgedQuery, 230 | L: 'static + Link, 231 | { 232 | let handle = use_query_value::>(input); 233 | let state = use_memo( 234 | |state| match state { 235 | QueryValueState::Loading => BridgedQueryValueState::Loading, 236 | QueryValueState::Completed { result } => BridgedQueryValueState::Completed { 237 | result: result 238 | .as_ref() 239 | .map_err(|e| e.clone()) 240 | .and_then(|m| m.inner.clone()), 241 | }, 242 | QueryValueState::Refreshing { last_result } => BridgedQueryValueState::Refreshing { 243 | last_result: last_result 244 | .as_ref() 245 | .map_err(|e| e.clone()) 246 | .and_then(|m| m.inner.clone()), 247 | }, 248 | }, 249 | handle.state().clone(), 250 | ); 251 | 252 | UseBridgedQueryValueHandle { 253 | inner: handle, 254 | state, 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run Tests & Publishing 3 | 4 | on: [push, pull_request] 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | lint: 11 | name: Lint Codebase 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Project 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Rust 18 | uses: dtolnay/rust-toolchain@stable 19 | with: 20 | targets: wasm32-unknown-unknown 21 | components: clippy 22 | 23 | - name: Configure sccache 24 | uses: futursolo/sccache-action@affix-token-on-demand 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Install cargo-make 29 | uses: davidB/rust-cargo-make@v1 30 | 31 | - name: Run Lints 32 | run: cargo make clippy 33 | 34 | rustfmt: 35 | name: Check Formatting 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Checkout Project 39 | uses: actions/checkout@v3 40 | 41 | - name: Setup Rust 42 | uses: dtolnay/rust-toolchain@nightly 43 | with: 44 | targets: wasm32-unknown-unknown 45 | components: rustfmt 46 | 47 | - name: Check Formatting 48 | run: cargo +nightly fmt -- --unstable-features 49 | 50 | check-templates: 51 | name: Check Templates 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: Checkout Project 55 | uses: actions/checkout@v3 56 | with: 57 | path: stellation 58 | 59 | - name: Setup Rust 60 | uses: dtolnay/rust-toolchain@stable 61 | with: 62 | targets: wasm32-unknown-unknown 63 | components: clippy 64 | 65 | - name: Configure sccache 66 | uses: futursolo/sccache-action@affix-token-on-demand 67 | with: 68 | token: ${{ secrets.GITHUB_TOKEN }} 69 | 70 | - name: Install Cargo Generate 71 | uses: taiki-e/install-action@v2 72 | with: 73 | tool: cargo-generate 74 | 75 | - name: Install cargo-make 76 | uses: davidB/rust-cargo-make@v1 77 | 78 | - name: Set Stellation Target to 'ci' 79 | run: | 80 | echo 'variable::set("stellation_target", "ci");' >> stellation/templates/default/resolve-crates.rhai 81 | 82 | - name: Generate Template 83 | run: | 84 | set -x 85 | mkdir templates-generated/ 86 | cd templates-generated 87 | 88 | for x in $(ls ../stellation/templates); do 89 | if [ -d ../stellation/templates/$x ]; 90 | then 91 | echo "Creating Template $x..." 92 | cargo generate --path ../stellation/templates/$x \ 93 | --name generated-$x 94 | fi 95 | done 96 | 97 | - name: Run Lints 98 | run: | 99 | set -x 100 | 101 | for x in $(ls); do 102 | cd $x 103 | cargo make clippy 104 | cd .. 105 | done 106 | 107 | working-directory: templates-generated 108 | 109 | publish: 110 | name: Publish to crates.io 111 | runs-on: ubuntu-latest 112 | needs: 113 | - lint 114 | - rustfmt 115 | - check-templates 116 | steps: 117 | - name: Checkout Project 118 | uses: actions/checkout@v3 119 | 120 | - name: Setup Rust 121 | uses: dtolnay/rust-toolchain@stable 122 | with: 123 | targets: wasm32-unknown-unknown 124 | components: rustfmt, clippy 125 | 126 | - name: Setup Python 127 | uses: actions/setup-python@v4 128 | with: 129 | python-version: 3.11 130 | 131 | - name: Configure sccache 132 | uses: futursolo/sccache-action@affix-token-on-demand 133 | with: 134 | token: ${{ secrets.GITHUB_TOKEN }} 135 | 136 | - name: Install cargo-make 137 | uses: davidB/rust-cargo-make@v1 138 | 139 | - name: Set Git Information 140 | run: | 141 | git config --global user.name "Stellation Actions" 142 | git config --global user.email "actions@stellation.dummy" 143 | 144 | - name: Prepare dry-run Registry 145 | if: "!startsWith(github.ref, 'refs/tags/')" 146 | run: | 147 | cargo install cargo-http-registry 148 | 149 | mkdir -p /tmp/dry-run-registry 150 | nohup cargo-http-registry /tmp/dry-run-registry & 151 | 152 | echo "CARGO_PUBLISH_EXTRA_ARGS=--registry=dry-run" >> $GITHUB_ENV 153 | 154 | echo "[registries.dry-run]" >> ~/.cargo/config 155 | echo 'index = "file:///tmp/dry-run-registry"' >> ~/.cargo/config 156 | echo 'token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"' >> ~/.cargo/config 157 | 158 | - name: Modify to publish to dry-run Registry 159 | if: "!startsWith(github.ref, 'refs/tags/')" 160 | run: | 161 | pip install tomlkit 162 | python3 ci/switch-registry.py 163 | 164 | git commit -a -m "chore: switch to dry-run registry" 165 | 166 | - name: Prepare crates.io Registry 167 | if: startsWith(github.ref, 'refs/tags/') 168 | run: | 169 | echo "CARGO_PUBLISH_EXTRA_ARGS=--token=${{ secrets.CRATES_IO_TOKEN }}" >> $GITHUB_ENV 170 | 171 | # Run lints first so it will be faster to run publish checks. 172 | - name: Run Lints 173 | run: cargo make clippy 174 | 175 | - name: Run cargo publish 176 | run: | 177 | CRATES=( 178 | stellation-core 179 | stellation-bridge 180 | stellation-backend 181 | stellation-backend-warp 182 | stellation-backend-tower 183 | stellation-backend-cli 184 | stellation-frontend 185 | stellation-stylist 186 | stctl 187 | stellation 188 | ) 189 | 190 | for s in "${CRATES[@]}"; 191 | do 192 | cargo publish \ 193 | ${{ env.CARGO_PUBLISH_EXTRA_ARGS }} \ 194 | --manifest-path crates/$s/Cargo.toml 195 | done 196 | 197 | env: 198 | RUSTFLAGS: "--cfg releasing" 199 | 200 | publish-templates: 201 | name: Publish Templates 202 | runs-on: ubuntu-latest 203 | if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) 204 | needs: 205 | - lint 206 | - rustfmt 207 | - check-templates 208 | steps: 209 | - name: Checkout Project 210 | uses: actions/checkout@v3 211 | 212 | - name: Read Stellation Version 213 | if: startsWith(github.ref, 'refs/tags/') 214 | run: | 215 | STELLATION_VER=$(echo '${{ github.ref_name }}' | sed 's/v*//') 216 | echo "Current version: $STELLATION_VER" 217 | 218 | echo "STELLATION_VER=$STELLATION_VER" >> $GITHUB_ENV 219 | 220 | - name: Set Stellation Target to 'main' 221 | if: github.ref == 'refs/heads/main' 222 | run: | 223 | echo 'variable::set("stellation_target", "main");' >> templates/default/resolve-crates.rhai 224 | 225 | - name: Set Stellation Target to 'release' 226 | if: startsWith(github.ref, 'refs/tags/') 227 | run: | 228 | echo 'variable::set("stellation_target", "release");' >> templates/default/resolve-crates.rhai 229 | 230 | - name: Set Stellation Version 231 | if: startsWith(github.ref, 'refs/tags/') 232 | run: | 233 | echo 'variable::set("stellation_release_ver", "${{ env.STELLATION_VER }}");' >> templates/default/resolve-crates.rhai 234 | 235 | - name: Publish Main Templates 236 | if: github.ref == 'refs/heads/main' 237 | uses: s0/git-publish-subdir-action@v2.6.0 238 | env: 239 | REPO: self 240 | BRANCH: templates-main 241 | FOLDER: templates 242 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 243 | MESSAGE: "chore: sync template for {sha}" 244 | 245 | - name: Publish Main Templates 246 | if: github.ref == 'refs/heads/main' 247 | uses: s0/git-publish-subdir-action@v2.6.0 248 | env: 249 | REPO: git@github.com:futursolo/stellation-templates.git 250 | BRANCH: stellation-main 251 | FOLDER: templates 252 | MESSAGE: "chore: sync template for {sha}" 253 | SSH_PRIVATE_KEY: ${{ secrets.TEMPLATE_DEPLOY_PRIVATE_KEY }} 254 | 255 | - name: Publish Release Templates 256 | if: startsWith(github.ref, 'refs/tags/') 257 | uses: s0/git-publish-subdir-action@v2.6.0 258 | env: 259 | REPO: self 260 | BRANCH: templates 261 | FOLDER: templates 262 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 263 | MESSAGE: "chore: sync template for ${{ github.ref_name }}" 264 | 265 | - name: Publish Release Templates 266 | if: startsWith(github.ref, 'refs/tags/') 267 | uses: s0/git-publish-subdir-action@v2.6.0 268 | env: 269 | REPO: git@github.com:futursolo/stellation-templates.git 270 | BRANCH: main 271 | FOLDER: templates 272 | MESSAGE: "chore: sync template for ${{ github.ref_name }}" 273 | SSH_PRIVATE_KEY: ${{ secrets.TEMPLATE_DEPLOY_PRIVATE_KEY }} 274 | --------------------------------------------------------------------------------