Repository navigation
Conversation
A new package that renders Fable.Lit templates to HTML on .NET, with no JavaScript runtime. The same html $"..." view compiles by Fable into lit templates and by the .NET compiler into HTML: both declare the Lit namespace, so a view file shared by the client and server projects needs no conditional compilation. render and toNode write plain HTML. renderHydratable and toHydratableNode also write the markers lit's own SSR writes (<!--lit-part DIGEST--> and <!--lit-node N-->), so @lit-labs/ssr-client's hydrate() adopts the markup in place. That needs two of lit's internals ported exactly: the template digest, and the way lit counts nodes to find attribute bindings. What a shared view may use is html, Ev, Lit.classes, Lit.ofList and Lit.nothing. Anything the server cannot honour raises UnsupportedTemplateValue rather than writing an approximation. That includes a Node put back into a template, which used to render as the text "HtmlTypeProvider.Node", and a <template> element in markup lit will adopt: lit counts it as one node and never looks inside, so every later binding would be looked for on the wrong node without any error. Output is built as HtmlTypeProvider Nodes, which supplies the escaping and lets a rendered view drop into a page template's hole. Tests: test/Lit.Server.Tests (xunit) checks the renderer and writes test/server-rendered.json from views in test/Shared/SharedViews.fs. The browser-side test/DifferentialTest.fs compiles the same views with Fable and checks, in a browser, that the server's markup matches lit's, that its node indices and digests equal lit's own, and that hydrate() adopts the markup and wires its handlers. test.yml runs dotnet test before npm test. The package has a CHANGELOG.md and is packed in the release job, so ShipIt releases it with the others. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OnurGumus
added a commit
to OnurGumus/Fable.Lit
that referenced
this pull request
Oct 28, 2026
Render.fs still opened by saying it does not hydrate and emits none of lit's markers, which stopped being true when renderHydratable arrived, and toHydratableNode pointed at a Lit.Server.md that has never existed. Both were found while porting the renderer upstream (fable-hub#88). The hydration sample's vite config gave its paths relative to the current directory, so the README's command, run from the repository root as it says, could not find Client/build/App.js. CI never built the bundle, so nothing noticed. Paths now resolve from the config file's own folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Fable.Lit.Server, a new package that renders Fable.Lit templates to HTML on .NET, with no JavaScript runtime involved.
The same
html $"..."view compiles two ways: by Fable into lit templates in the browser, and by the .NET compiler into HTML on the server. Both packages declare theLitnamespace, so a view file shared by the client and server projects needs no conditional compilation:This is the first of a few PRs. Later ones would add a client-side helper that adopts server markup and falls back to rendering, Elmish programs that start from server markup, declarative shadow roots, and server-rendered
LitElements. They're all built and tested in my fork (OnurGumus/Fable.Lit), and a demo using them runs at https://litdemo.novian.works. I'm sending them in pieces so each one can be reviewed on its own. This one is server-side only, plus the tests that hold it to lit's behaviour.What it does
render/toNode: plain HTML, which the browser renders over.renderHydratable/toHydratableNode: also write the markers lit's own SSR writes (<!--lit-part DIGEST-->,<!--lit-node N-->), so lit'shydrate()adopts the markup in place: the elements stay, and lit attaches its bindings to them. That needs two of lit's internals ported exactly: the template digest, and the way lit counts nodes to find attribute bindings.@eventand.propertybindings have no HTML form and are left out.?boolattributes are written or left out.html,Ev,Lit.classes,Lit.ofList,Lit.nothing. Anything else the server can't honour raisesUnsupportedTemplateValueinstead of writing an approximation. Two mistakes that would otherwise fail silently are refused:Nodeput back into a template, which would render as the textHtmlTypeProvider.Node;<template>element in markup lit will adopt. lit counts it as one node and never looks inside, so every binding after it is looked for on the wrong node, without any error.Why HtmlTypeProvider
Output is built as HtmlTypeProvider
Nodes. That supplies the escaping, and lets a rendered view drop into a page template's hole as a typed value rather than a string. Disclosure: it's my package. The dependency is small (FSharp.Core only), but if you'd rather Fable.Lit.Server had no third-party dependency, it could be replaced with a string builder and a few escaping functions.Tests
The renderer is only worth anything if it agrees with lit, so most of the testing is a comparison against lit itself:
test/Lit.Server.Tests(xunit, 23 tests): the renderer's own cases, including the refusals. It also writestest/server-rendered.jsonfrom the views intest/Shared/SharedViews.fs.test/DifferentialTest.fs(browser, 4 tests): compiles the sameSharedViews.fswith Fable and checks, in Chrome, that:_getTemplateHtml);digestForTemplateResultfrom@lit-labs/ssr-client;hydrate()adopts the server's markup without replacing elements, and its click handler fires.A wrong digest passes the .NET tests and fails two of these; I checked by breaking it on purpose.
test.ymlgets adotnet teststep beforenpm test, because the browser test reads the JSON the .NET tests write. Runningnpm testalone in a fresh checkout fails withmissing /test/server-rendered.jsonrather than passing quietly.@lit-labs/ssr-clientis added as a dev dependency, for the tests only.Other changes
Fable.Lit.sln: the two new projects.src/Lit.Server/CHANGELOG.mdwith ShipIt front matter, and adotnet packline in the release job. With noforce_version, I expect ShipIt to start it at 0.1.0. If you'd rather it start at 2.0.0 with the others, that's a one-line change.docsrc/docs/server-rendering.md.net10.0, because HtmlTypeProvider does.dotnet build Fable.Lit.sln,dotnet test test/Lit.Server.Tests(23 passed) andnpm test(41 passed) all pass locally, in the order CI runs them.dotnet packproducesFable.Lit.Serverwith upstream's metadata.🤖 Generated with Claude Code