feat: custom renderers API#18042
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
so does this mean that we can port our web apps to mobile with a custom renderer ? or does someone need to make a custom renderer to do that ? :D |
You still need a target that supports native. Lynx is definitely a good option, especially with CSS support. That said, mobile has generally some strict rules about the shape of the code. For example, in Lynx (like in React Native) you can't have text without a wrapping text element. You can write a custom renderer to map from web elements to lynx elements, but that will probably not work out of the box. That said, once we have the Lynx or Svelte Native renderer, you will be likely able to write a mobile app that also renders to Web since they support it. |
|
transition: in: and out: should work with the renderers, we can just create javascript based transitions instead of css ones |
Even if you limit them to JS based ones...how do you transition something in the terminal? How do you transition something in Lynx? How you do it if the renderer just renders a JS object? The concept of a transition itself is just something that applies to mostly DOM. There might be some way to allow the creators of the renderers to transition in and out but honestly I can't think of an API for that...any ideas? |
No, it is not DOM specific, js transitions have a tick function, what the user does with that is user's choice, you could use Element PAPI to change a property of an element in every tick |
I guess we could enable them AND throw a descriptive error in case you try to build a css one 🤔 A small issue would be that you would not be able to use the backed in transitions but I assume we could just document that. |
|
Ok I took a look: imho is not worth doing this in the first draft. Transitions, even with the tick function, are still coordinated by |
|
strong agree: transitions in their current form are absolutely not something we want to bring with us |
|
Not sure if this is doable but wanted to bounce some thoughts after reading the conversation. Are lifecycle hooks like onMount/onDestroy exposed to renderer authors at all? Would it be feasible to add an optional async hook like beforeUnmount/beforeDestroy on the renderer API itself? Something that returns a Promise the runtime would await before actually removing the node/fragment. The idea is that renderer authors could use it to handle whatever they need like animations but without the renderer API having to commit to transition: in: and out:. Since it's optional, the DOM renderer just doesn't implement it at all. |
|
Adding new lifecycle hooks to prevent the destruction of an effect is exactly what we'd planned, yes — the long term goal is to provide a way to do transition-like things in attachments, precisely so that component authors have more programmatic control (initially for the sake of building more complex layout transitions/physics-based transitions/staggered transitions/etc, but it also extends the capability to custom renderers). No timeframe on that though, as our energy has been focused on async stuff lately. |
Closes #15470
We're so back!
Finally, thanks to Syntax and SuppCo that sponsored the Custom Renderers initiative, I was able to work full-time for a bit on this (thanks again to my employer, Mainmatter for allowing me to do this).
This, the fact that we recently revisited the direction we were going (that lead to much less code needed), plus the fact that Claude now is decently good at navigating the Svelte codebase allowed me to do a lot of progress (I was also able to re-use part of the code I already had before).
As you can see, this is a big PR, but I tried to split the commits reasonably so that the review process shouldn't be too bad and there wasn't really a way to verify it worked before having built most of it. There are still a few To-dos but luckily, I also have a few more days to work on this (and at this point is in a place where I can also do it in my spare time).
To-dos
How it works
experimental.customRendereris a new compile configuration option. It can be astringor a function that accepts the filename and returns astring. The value should be an NPM package or a module that exports the renderer as its default export. When this option is defined, the compilation output changes a bit (no delegated events, no inlining ofnode.nodeValue="", no customizable select, etc...basically we're doing a lot of optimization that are specific to the DOM which are skipped).The compilation also changes because now the compiled component imports the module you specify, uses
from_treeinstead offrom_html, push the renderer at the beginning of the component and pop it at the end...basically thisgets compiled to
What does a custom renderer looks like? You can have a look at the one I created for testing in
packages/svelte/tests/custom-renderers/renderer.jsto have a more practical example but basically, you can importcreateRendererfromsvelte/rendererand then specify a series of DOM-like operations in your "world".You can then use the return value to "mount" your component
A good custom renderer is crucial to make svelte works properly so we will need to document this correctly (even though I don't expect people to create custom renderers in their day to day and the Svelte team will likely be the primary user of this API).
A few examples of this:
insertassumes that the insertion works like the DOM: if you insert something that already has a parent it should be removed from where it is.insert-ing a fragment means inserting all the children of the fragment in the parent.Limits
A few features of Svelte are designed specifically for the DOM and thus are disabled if you try to compile a component with a custom renderer:
bind:on regular elements is forbidden, since svelte register known DOM events to keep the variables in sync.transition:,animate:,in:andout:are forbidden, since, once again, those use DOM manipulation under the hood.svelte:window,svelte:body,svelte:document,svelte:head... I mean, do I really need to explain why this is forbidden?css: injectedis also forbidden since it appends thestyletag to the documentcreateRawSnippetthrows a runtime error since it relies on thetemplatetag to generate the HTML elements from the string you returnAnother quirk is that you can technically interleave components compiled with different renderers (imagine a DOM component into a Threlte one) but:
beforefunction on the comment that will receive the fragment/element/text that the component is trying to "mount")@rendera snippet compiled with a different renderer from the one that is invoking@render. This means if I'm mounting a DOM component into a Threlte component I can't pass a snippet (unless that snippet is exported from a component compiled without a renderer and imported into the Threlte component)These limitations might change before we merge if we find a way to make them work.
What this PR does?
The main job of this PR is to centralize every DOM operation in
operations.jsas an exported function. This allows the function to check if arendereris available so that it can call the method on the renderer instead of the DOM method. The renderer is also captured in every effect created in the component, since it needs to be "pushed" again before the effect execute. The same is true for boundaries, batches and each blocks.I've also added a somewhat decent test suite that uses a render-to-object renderer that renders the svelte components to...well...an object. This allows the tests to be "similar" to the rest of the test suite (there's even an object-to-HTML string helper to assert the shape of the component) but is executed in a node environment, so every access to DOM API will actually throw.
I've changed some of the validation in place, but there's still a few warnings and errors that don't make sense, which I plan to fix before merging.
A few questions
objectcould help us with the maintainability of the custom renderers API (now Typescript will yell at us if we try to accesselement.valuewithout checking)...but it could make the maintainability of DOM Svelte a nightmare (because now you have to check everything and everywhere). Should we keep it a lie?Extra
To test this out, I (admittedly Claude) built an opentui custom renderer to render svelte component to the terminal...here's a small preview.
Screen.Recording.2026-03-31.at.15.17.13.mp4