,
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