feat(routing): Preserve query-aware navigation and partial template delivery#109
Merged
mohamedmansour merged 3 commits intomainfrom Mar 14, 2026
Merged
feat(routing): Preserve query-aware navigation and partial template delivery#109mohamedmansour merged 3 commits intomainfrom
mohamedmansour merged 3 commits intomainfrom
Conversation
…-aware partial template delivery
65faa04 to
abc19dd
Compare
mohamedmansour
commented
Mar 14, 2026
Contributor
Author
mohamedmansour
left a comment
There was a problem hiding this comment.
Fixed code review
Comment on lines
+55
to
+61
| IntPtr resultPtr = NativeBindings.webui_handler_render( | ||
| _handle, | ||
| protocol, | ||
| (nuint)protocol.Length, | ||
| stateJson, | ||
| entryId, | ||
| requestPath); |
Comment on lines
+90
to
+95
| IntPtr resultPtr = NativeBindings.webui_get_route_templates( | ||
| protocol, | ||
| (nuint)protocol.Length, | ||
| entryId, | ||
| requestPath, | ||
| inventoryHex); |
|
|
||
| protected override bool ReleaseHandle() | ||
| { | ||
| webui_handler_destroy_raw(handle); |
| internal static WebUIHandlerSafeHandle CreateHandler(string? pluginId) | ||
| { | ||
| IntPtr handle = pluginId is null | ||
| ? webui_handler_create_raw() |
| { | ||
| IntPtr handle = pluginId is null | ||
| ? webui_handler_create_raw() | ||
| : webui_handler_create_with_plugin_raw(pluginId); |
| } | ||
|
|
||
| [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "webui_handler_create")] | ||
| private static extern IntPtr webui_handler_create_raw(); |
| private static extern IntPtr webui_handler_create_raw(); | ||
|
|
||
| [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "webui_handler_create_with_plugin")] | ||
| private static extern IntPtr webui_handler_create_with_plugin_raw( |
| [MarshalAs(UnmanagedType.LPUTF8Str)] string? pluginId); | ||
|
|
||
| [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "webui_handler_destroy")] | ||
| private static extern void webui_handler_destroy_raw(IntPtr handlerPtr); |
janechu
approved these changes
Mar 14, 2026
Contributor
janechu
left a comment
There was a problem hiding this comment.
I think we might try adding information for the agents about unmanaged code being part of the design so they could perhaps stop leaving those comments. Also I would like to see some mermaid diagrams in future, GitHub will render these.
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.
Scope
This staged set is doing two tightly related things:
Why we changed this
1. Query-driven routes need two path concepts
Apps like the commerce example use query parameters as real state, especially for search and sorting (
q,sort, etc.).That means the client router and the server cannot treat "the route path" and "the full request URL" as the same thing:
Without that split, the router can match the correct route but still ask the server for the wrong state, which causes SSR and hydrated navigation to drift apart.
2. Partial-template delivery needed to become reusable
Before this change, the partial-template logic was mostly centered in
crates\webui-cli\src\commands\serve.rs.That worked for the built-in dev server, but it had two problems:
Because this is library/framework behavior, the real implementation belongs in
webui-handler, with the CLI acting as a thin caller.3. We wanted route awareness without state-aware pruning
This change intentionally does not evaluate runtime
ifconditions, loop collections, or component-prop state while deciding which templates to ship.That is deliberate.
Once the client hydrates, the client becomes authoritative for state changes. Template shipping should therefore behave like a capability bundle for:
It should not try to mirror a transient server-state snapshot and risk under-shipping templates the client may need immediately after hydration.
At the same time, we still do not want to walk every sibling
<route>branch. So the chosen design is:High-level outcome
After this staged change:
pathnameandrequestPath.pathname.requestPath, so query strings are preserved.webui-handlerwebuiwebui-ffiEnd-to-end flow after the change
Client side
NavigationTargetcontaining:pathname: for local route matchingrequestPath: for fetch/history, including query stringpathname.requestPath.X-WebUI-Inventory.Server side
webui-cli servesplits the incoming path into:route_path: pathname onlyrequest_path: pathname plus query stringroute_path.request_path.webui-handler.Template discovery
The request-aware helper:
component,if,for, and attribute-template edges conservatively<route>fragments, it picks only the best match for the current request pathWhat changed, file by file
Router package
packages\webui-router\src\navigation-path.tsThis file introduces the explicit split between:
pathnamerequestPathKey behavior:
buildNavigationTarget()strips the base path fromurl.pathnamerequestPathis built aspathname + url.searchprependBasePath()works on the full request path, so queries are not dropped on navigation or fallbackThis is the core client-side fix that keeps route matching and request-state resolution separate.
packages\webui-router\src\router.tsThis file applies the new path model everywhere it matters:
handleNavigation()matches usingpathnamerequestPathfetchAndMount()fetchesrequestPathwebui:route:navigatedevent now reports the full request pathX-WebUI-InventoryThis is the behavioral bridge between the router fix and the server-side partial-template work.
packages\webui-router\src\types.tsThis updates the public type surface to match the new behavior, especially the fact that navigation event paths may include query strings.
packages\webui-router\test\navigation-path.test.tsAdds focused tests for:
/packages\webui-router\test\navigation-path.test.jsThis is the bundled test artifact used by the package test script.
packages\webui-router\package.jsonAdds the explicit navigation-path test flow so the router package validates the new URL helper behavior.
Framework route-template selection
crates\webui-handler\src\route_handler.rsThis is the center of the staged change.
The file now provides two request-aware helpers:
get_needed_components_for_request(...)get_route_templates_for_request(...)The implementation keeps the existing inventory model, but adds request-aware traversal on top of the existing fragment graph.
Important design points:
if,for, and attribute-template edges are still followed conservativelyThe helper deliberately avoids "state-aware discovery". It over-ships within the active route chain rather than risking under-shipping after hydration.
This file also adds targeted tests covering:
CLI integration
crates\webui-cli\src\commands\serve.rsThis file now acts as a thin integration layer instead of owning the core partial-template algorithm.
Relevant changes:
RequestPathsnow clearly separatesroute_pathandrequest_pathbuild_request_paths()preserves query stringshandle_json_partial():request_pathroute_pathwebui_handler::route_handler::get_needed_components_for_request(...)The key architectural improvement here is that
serve.rsno longer invents its own route + entry union strategy. It now relies on the framework helper rooted at the entry fragment.Rust public API
crates\webui\src\lib.rsThe new request-aware helpers are re-exported from the top-level Rust crate:
get_needed_components_for_requestget_route_templates_for_requestThis matters because it makes the behavior available to custom Rust hosts without forcing them to depend on internal crate layout.
FFI surface
crates\webui-ffi\src\lib.rsThe FFI route-template API is updated from an entry-only shape to a request-aware shape.
webui_get_route_templates(...)now accepts:protocol_dataprotocol_lenentry_idrequest_pathinventory_hexBehavioral changes:
get_route_templates_for_request(...)helperstd::panic::catch_unwind()so panics cannot cross the FFI boundarytemplatesand updatedinventoryWhile touching the FFI surface,
set_last_error(...)was also tightened so it no longer depends onunwrap/expectwhen preparing the thread-local error string.crates\webui-ffi\include\webui_ffi.hThe header is kept in sync with the Rust ABI by adding the new
request_pathparameter and updating the function documentation to describe active-route-chain selection.crates\webui-ffi\tests\ffi_test.rsAdds an integration test that verifies the FFI entrypoint returns templates only for the active route chain, not unrelated sibling routes.
.NET host surface
dotnet\src\Microsoft.WebUI\NativeBindings.csUpdates the P/Invoke declaration of
webui_get_route_templates(...)to includerequestPath.dotnet\src\Microsoft.WebUI\WebUIHandler.csUpdates the public wrapper method:
GetRouteTemplates(protocol, entryId, inventoryHex)GetRouteTemplates(protocol, entryId, requestPath, inventoryHex)This keeps the .NET API aligned with the Rust and C ABI changes.
Specification
DESIGN.mdThe specification now describes the intended partial-template behavior explicitly:
if/forbranches within the active route chain may still be over-shipped intentionallyThis matters because the behavior is not just an implementation detail anymore; it is part of the framework contract.
Important implementation decisions
We reused the existing route matcher instead of inventing a second one
The request-aware helper uses the same route matching semantics already used by SSR. That keeps server rendering and partial-template selection aligned.
We did not add new protocol fields
This change does not introduce a new discovery graph or protocol format. It reuses the existing fragment graph plus route matching.
That keeps the solution simpler, easier to reason about, and cheaper to propagate across hosts.
We kept inventory filtering deterministic
The inventory bitset continues to use the FNV-1a-based component hashing model so client and server stay aligned on what is already loaded.
We scoped route pruning, not state pruning
The crucial distinction in this change is:
That is the core "why" of the chosen design.
Validation performed
The staged change was validated with:
What the next reader should keep in mind
If you need to change this area later, the main invariants are:
If any future change breaks one of those invariants, expect SSR, hydration, or client navigation to drift apart again.