Skip to content

feat(observe): declare the route the document arrived on - #623

Merged
ryansolid merged 5 commits into
nextfrom
feat/declare-initial-route
Sep 30, 2026
Merged

ryansolid merged 5 commits into
nextfrom
feat/declare-initial-route

Conversation

@ryansolid

@ryansolid ryansolid commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Stacked on #641 (base branch migrate-public-hydration-api); GitHub retargets to next when that merges. solid-js / @solidjs/web 2.0.0-rc.13 ship NavigationRef.initial and RenderEvent.route (solidjs/solid#3683, in rc.12), so this branch bumps to ^2.0.0-rc.13 and moves the peer floor to ^2.0.0-rc.12. Verified on the published packages: pnpm test client 40 files / 445 tests, server 7 / 44, type tests, pnpm build, attw all green.

What

The router already declares every navigation to the observe tier around its location write (OBSERVE.attribution.withOrigin), so consumers get the route pattern for every navigation — except the first. A fresh document has no location write, so the route the page loaded as was never declared, and on the server the request had a URL and no route.

Now RouterComponent wraps the work that builds its context in the same call, with initial: true on the ref:

const routerState = OBSERVE
  ? OBSERVE.attribution.withOrigin(
      describeInitial(matchPath, untrack(integration.signal[0]).value),
      buildContext
    )
  : buildContext();
  • Client: the engine opens the frame at the time origin and settles it as the context is built — the first "navigation" record, initial: true, writes: 0, at: 0, no from. Kept out of feedback().navigations by the engine.
  • Server: there is no engine; the same call files the ref on the render, and the request's "render" record carries RenderEvent.route ({ name, to, params }), read at settle so a lazy subtree that resolved during the render names the exact route (pinned).
  • describeNavigation and describeInitial share the match description (routeName, routeParams); name/params remain getters read at settle.
  • Folded out of shipped bundles with OBSERVE, as the navigation declaration is.

Also in this PR (rc.10 moves the touched files need)

  • NavigationRef is imported from solid-js (the frame refs moved beside withOrigin in rc.10).
  • test/observe-navigation.spec.tsx uses attribution.history("navigation") in place of the removed attribution.navigations().

Public API

No router option or export changes; one new call site. Peer dependency floor: solid-js / @solidjs/web peers move from ^2.0.0-rc.10 to ^2.0.0-rc.12, the first release with the fields this declaration uses (and without the sharedConfig types #641 replaces).

Tests

  • test/observe-navigation.spec.tsx +3: mounting declares the initial route (fields, at: 0, writes: 0, committed); an empty arrival normalized onto base is that one record (writes: 1), not a navigation; the declaration is not a feedback().navigations row.
  • test/server/observe-render-route.spec.tsx (new, 6): url prop, root /, request-event URL precedence, lazy subtree refined at settle (stream path), empty arrival + base names the render by the base, no router → no route.

Why it matters

Sentry's Performance product (and every peer framework SDK) names transactions by route pattern; @sentry/solid-2 will rename the pageload root from the initial record and set http.route from render.route. Any other Solid router adopts the same contract with the same one call.

— Claude via Cursor

Made with Cursor

@changeset-bot

changeset-bot Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f1f0772

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solidjs/router Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ryansolid

ryansolid commented Sep 28, 2026 •

Copy link
Copy Markdown
Member Author

Follow-up pushed (b5a1cc8): the base-normalization write. With base set and an empty arrival (hash history with no hash, memoryHistory(""), a static render with no url) createRouterContext writes the base path inside the wrapped build; that write went through the setter's withOrigin, opened a nested navigation frame, and took the write — so the initial record kept to: "" (matched to nothing → name: "/") and a phantom "" → /app navigation followed. Now a write from no location is not declared as a navigation (the arrival is already declared by the initial frame, which the write then stamps), and the initial ref's fields are getters read at settle, so the record is one, to: "/app", name: "/app", writes: 1, committed. Pinned on both sides (memoryHistory("") + base client; no-url static render + base server).

— Claude via Cursor

ryansolid and others added 3 commits September 29, 2026 23:49
The same withOrigin call the router makes for every navigation, around
the work that builds its context, with `initial: true` on the ref — on
both sides. Client: the first "navigation" record names the pageload's
route. Server: the request's "render" record carries RenderEvent.route.
`describeNavigation` and the new `describeInitial` share the match
description (`routeName`/`routeParams`).

Also the rc.10 moves the observe test and this file need: NavigationRef
is exported from `solid-js`, and `attribution.history("navigation")`
replaces `attribution.navigations()`.

Requires the solid release shipping NavigationRef.initial and
RenderEvent.route (solidjs/solid#3683).

Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…l record, not a navigation

The base normalization write (a hash history with no hash, a memory
history seeded with "") went through the setter's withOrigin and opened a
nested navigation frame that took the write, so the initial record kept
the pre-normalization location — `to: ""`, matched to nothing — and a
phantom navigation "" -> base followed it. A write from no location is
not a navigation: the setter no longer declares it, the write stamps the
enclosing initial frame (writes: 1, settles at drain end), and every field
of the initial ref is a getter read at settle, so `to`/`name`/`params`
are the normalized location on both sides.

Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
NavigationRef.initial and RenderEvent.route shipped in rc.12; rc.13 is current.
The peer floor moves to rc.12, the first release with the fields this
declaration uses (and without the sharedConfig types the migration replaces).

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid force-pushed the feat/declare-initial-route branch from 94713bc to 1d21adf Compare September 30, 2026 06:50
@ryansolid
ryansolid changed the base branch from next to migrate-public-hydration-api September 30, 2026 06:50
@ryansolid
ryansolid marked this pull request as ready for review September 30, 2026 06:50
@ryansolid

Copy link
Copy Markdown
Member Author

rc.13 is out, so this is no longer waiting on a Solid release. Rebased onto #641 (migrate-public-hydration-api) and retargeted there — rc.12 dropped sharedConfig from solid-js's public types, so the bump this PR needs cannot land on next before that migration; GitHub retargets to next when #641 merges. Added 1d21adf: solid-js / @solidjs/web ^2.0.0-rc.13, peer floor ^2.0.0-rc.12; #641's planned bump commit will resolve to the same lines.

Verified on the published packages (no linked runtime): client 40 files / 445 tests, server 7 / 44, type tests, pnpm build, @arethetypeswrong/cli all green — the flash-seeding failures noted earlier were the linking artifact and do not occur on rc.13. Marked ready for review.

— Claude via Cursor

@ryansolid
ryansolid changed the base branch from migrate-public-hydration-api to next September 30, 2026 07:25
ryansolid and others added 2 commits September 30, 2026 00:25
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	package.json
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit aec5796 into next Sep 30, 2026
4 checks passed
@ryansolid
ryansolid deleted the feat/declare-initial-route branch September 30, 2026 21:36
brenelz pushed a commit to ryansolid/tanstack-router that referenced this pull request Oct 5, 2026
…d's observe tier

The Solid adapter declared every navigation's match publish to the observe
tier, but the first page has no publish to wrap, so the route the page
loaded as was never declared, and on the server the request's "render"
record had a URL and no route. solid-js / @solidjs/web 2.0.0-rc.12 added
NavigationRef.initial and RenderEvent.route for exactly this; Solid Router
adopted them in solidjs/solid-router#623.

RouterContextProvider now wraps the work that establishes its initial
match (the ready memo, which on the server starts the load) in the same
call, with initial: true on the ref:

- Client: the engine opens the frame at the time origin and settles it as
  the provider returns: the first "navigation" record, initial: true,
  writes: 0, at: 0, no from; kept out of feedback().navigations.
- Server: there is no engine; the server entry's withOrigin files the ref
  on the render, and the "render" record carries RenderEvent.route
  ({ name, to, params }), read at settle, after a provider-run load.

describeInitial shares describeRoute with describeNavigation; its fields
are getters over router.latestLocation.

The Transitioner's post-mount commit of the canonical arrival location
(search defaults, trailing slash) is the arrival's, not a navigation, the
TanStack form of TanStack#623's "an empty arrival normalized onto the base". With
the router loaded before mount, resolvedLocation was already set and that
commit was declared as a navigation from the arrival to itself; without
it, the arrival's history change left requestedAt set and the next real
navigation was dated from the mount. The commit is now marked, and every
non-pending publish answers the pending request, declared or not.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant