leptos-rs / leptos

Build fast web applications with Rust.
https://leptos.dev
MIT License
16.39k stars 655 forks source link

Allow server components in islands architecture to call code behind ssr without need for #[server] #2578

Open versecafe opened 6 months ago

versecafe commented 6 months ago

Is your feature request related to a problem? Please describe. It's a bit annoying to need to worry about the security implications of exposing every server function when some functions should be server accessible only with no exposure for components rendered only on the server, same behaviour as server side code with RSCs not needing to be exposed to clients

Describe the solution you'd like inside of a #[component] macro when experimental-islands enabled allow all ssr dependencies

#[cfg(features="ssr")]
async fn db_example() -> Result<Vec<String>, ServerFnError> {
    use sqlx::Row; // sqlx is under ssr deps in cargo.toml

    let url: &str = "example-pg-url";
    let pool = sqlx::postgres::PgPool::connect(&url).await?;

    let res = sqlx::query("SELECT `title` FROM `works`").fetch_all(&pool).await?;
    let titles: Vec<String> = res.get()
    return titles;
}

#[component]
fn ServerOnly() -> impl IntoView {
    view! {
      <div>
        <Await future=|| db_example() let:data>
          <For
            each=move || { res.clone().into_iter().enumerate() }
            key=|(index, data)| *index
            // renders each item to a view
            children=move |val| {
                let title = val.to_string();
                view! { <p>{title}</p> }
            }
          />
        </Await>
      </div>
    }
}

Describe alternatives you've considered Just always making a server action even if it's never used on the client and handling the security for all of them as if they were exposed endpoints rather than internal functions.

Additional context Async RSC example as a reference over from the JS/TS world

async function getTitles(): Promise<string[]> {
  // drizzle ORM example as comparison to sqlx
   return await db
    .select({
      title: worksTable.title
    })
    .from(worksTable);
}

async function ServerOnly(): Promise<JSX.Element> {
  return (
    <div>
      {titles.map((title) => (
        <p>{title}</p>
      )}
    </div>
  );
}
gbj commented 6 months ago

This is not unreasonable. The way it works how it currently works is this: when compiling the frontend in islands node, the compiler actually compiles the whole application, then does dead code elimination to remove anything that's not used in the body of an island.

This is because #[component] does not mean "guaranteed to run on the server"; an island can use a component in its body and that will be included in the WASM binary. We don't have server components guaranteed only to be on the server; we have islands guaranteed to be on the client, and shared components.

A straightforward workaround is to cfg-flag whatever server-only code you want to call in a component, and #[cfg(not(feature= "ssr"))] let foo = unreachable!(); if you are sure you don't use it in an island.