withastro / astro

The web framework for content-driven websites. ⭐️ Star to support our work!
https://astro.build
Other
46.81k stars 2.49k forks source link

Astro slots passed as Svelte 5 snippets don't statically render #10512

Closed c9r closed 1 week ago

c9r commented 8 months ago

Astro Info

Astro                    v4.5.8
Node                     v21.7.1
System                   macOS (arm64)
Package Manager          pnpm
Output                   hybrid
Adapter                  @astrojs/cloudflare
Integrations             @astrojs/mdx
                         @astrojs/svelte
                         @astrojs/sitemap

If this issue only occurs in one browser, which browser is a problem?

No response

Describe the Bug

When statically generating (or server-side rendering) a .astro page that contains a Svelte 5.0.0-next.80 component, static children of the Astro instantiation don't statically render as snippets in the Svelte component. Below is a minimal example, followed by a likely root cause, and a potential fix.

Distilled example

Given the following Svelte 5 component, in button.svelte:

<script>
  const { children } = $props();
</script>

<button>
  {@render children()}
</button>

included from an Astro component, page.astro:

---
import Button from "./button.svelte";
---

<Button>Click Me!</Button>

the following static HTML gets rendered (note the absence of a "Click Me!" child text node):

<!DOCTYPE html><!--ssr:0--><button><!--ssr:1--><!--ssr:1--></button><!--ssr:0-->

Root cause

Take a look at the JS output for the aforementioned button.svelte component (playground link):

 // App.svelte (Svelte v5.0.0-next.80)
 // Note: compiler output will change before 5.0 is released!
 import * as $ from "svelte/internal/server";

 export default function App($$payload, $$props) {
   $.push(true);

   const { children, ...attrs } = $$props;
   const anchor = $.create_anchor($$payload);

   $$payload.out += `<button${$.spread_attributes([attrs], true,  false, "")}>${anchor}`;
+  children($$payload);
   $$payload.out += `${anchor}</button>`;
   $.bind_props($$props, { children });
   $.pop();
 }

Note that, in the compiled Svelte 5 component above, the children snippet gets invoked as a side-effecting function with a $$payload parameter.

But if you look at server-v5.js, the bridged snippet functions created by renderToStaticMarkup simply return the interpolated Astro slots, rather than appending the rendered output to $$payload.out (as appears to be the expectation, from a quick poke around the Svelte 5 codebase). Because the return value of the snippet function is unused by the compiled version of the Svelte component (at least as of v5.0.0-next.80), the child content of the Astro instantiation of the Svelte component simply gets discarded.

server-v5.js#L25-28

async function renderToStaticMarkup(Component, props, slotted, metadata) {
   // ...
   let children = undefined;
   let $$slots = undefined;
   for (const [key, value] of Object.entries(slotted)) {
     if (key === 'default') {
+      children = tagSlotAsSnippet(() => `<${tagName}>${value}</${tagName}>`);
     } else {
       $$slots ??= {};
+      $$slots[key] = tagSlotAsSnippet(() => `<${tagName} name="${key}">${value}</${tagName}>`);
     }
   }
   // ...
}

Potential fix

Modifying the snippet functions generated by renderToStaticMarkup in server-v5.js to append their output to $$payload.out, instead of returning it, leads to proper static rendering of the above button example.

server-v5.js

async function renderToStaticMarkup(Component, props, slotted, metadata) {
   // ...
   let children = undefined;
   let $$slots = undefined;
   for (const [key, value] of Object.entries(slotted)) {
     if (key === 'default') {
-      children = tagSlotAsSnippet(() => `<${tagName}>${value}</${tagName}>`);
+      children = tagSlotAsSnippet(($$payload) => $$payload.out += `<${tagName}>${value}</${tagName}>`);
     } else {
       $$slots ??= {};
-      $$slots[key] = tagSlotAsSnippet(() => `<${tagName} name="${key}">${value}</${tagName}>`);
+      $$slots[key] = tagSlotAsSnippet(($$payload) => $$payload.out += `<${tagName} name="${key}">${value}</${tagName}>`);
     }
   }
   // ...
}

What's the expected result?

Svelte 5 components should statically render children passed to them from Astro component instantiations. Note that the expected output includes the "Click Me!" text node that is absent from the currently generated output.

expected output

<!DOCTYPE html><!--ssr:0--><button><!--ssr:1-->Click Me!<!--ssr:1--></button><!--ssr:0-->

Link to Minimal Reproducible Example

https://stackblitz.com/edit/github-rw1mr4

Participation

matthewp commented 7 months ago

@bluwy do we need to do something to suppose Svelte snippets?

bluwy commented 7 months ago

Yeah the issue proposed solution seems fine to me. I'm pretty sure I've fixed this before (https://github.com/withastro/astro/pull/9285) but Svelte might've changed the implementation again.

und3fined commented 6 months ago

Svelte 5 in RC from Apr 30 2024 any merged for this fix?

Lofty-Brambles commented 2 months ago

@bluwy - any ETA on this? 😅 Named slots don't work in Svelte 5 either, the same bug occurs

RATIU5 commented 1 month ago

I am also running into the same issue.

Arecsu commented 1 week ago

up this issue

ematipico commented 1 week ago

PRs are welcome, and OP is willing to send a PR

bluwy commented 1 week ago

This should be fixed by https://github.com/withastro/astro/pull/12328

bluwy commented 1 week ago

Fixed in @astrojs/svelte 5.7.3