Skip to content

Head and history interfaces, and interceptors over every host import - #25

Merged
lannbot merged 1 commit into
mainfrom
feat/platform-interceptors
Sep 5, 2026
Merged

Head and history interfaces, and interceptors over every host import#25
lannbot merged 1 commit into
mainfrom
feat/platform-interceptors

Conversation

@lannbot

@lannbot lannbot commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Follow-up to #23/#24, covering the remaining platform gaps against dioxus-web and the customization hook polyvisor needs.

New WIT (wit/world.wit), both unconditional imports:

  • head: set-title, create-element(tag, attrs, contents) -> bool. Retires the "head elements need eval" gap. Default host refuses script unless eval is granted.
  • history: dioxus-history's trait over abstract routes; changes() -> stream<string> carries host-initiated moves (back button, visor navigation). Encoding is host-side: MountOptions.history: "memory" | "fragment". Hydration note in the doc: a hydrating host must answer the prerendered route first.
  • eval.error.denied → guest maps to EvalError::Unsupported (the graceful path apps already handle).

Interceptors (host/src/intercept.ts, normative header): MountOptions.intercept.{dom,eval,head,history} — per-operation (next, ...args) => result. Typed against each fragment's import table; denial has one spelling per return type; gate bracket is outside the interceptor (fragments wrap their unbracketed impls then bracket); throwing = host bug (trap); intercept.eval without eval: true throws at mount.

Example polyvisor policy:

intercept: {
  eval: { eval: (next, js, send) => ALLOWED.has(hash(js)) ? next(js, send) : evalDenied() },
  head: { setTitle: (next, t) => next(`${app}${t}`) },
  history: { external: () => false },
}

Guest: WitDocument gains set_title/create_head_element (queue_effect'd as dioxus-web does; dioxus-document is now a plain dependency, eval still feature-gated); new src/history.rs WitHistory with updater reading changes. Both provided as root context.

Tests: unit tests for wrap, head, history (memory provider, changes, interceptor refusal); platform_component_test.ts drives the real fixture through eval (incl. denial), head (incl. script refused via interceptor), and router (click + host-driven forward()). 142 deno tests; full just check/fixtures/example/ssg-example/serve-test/test pass.

Not covered here (documented gaps remaining): clipboard/file payloads, suspense in hydration.

Automerge armed (merge commit).

Two more platform interfaces, both unconditional imports of `world app`:

- `head`: `set-title` and `create-element`, the back end of dioxus's
  `document::Title`/`Meta`/`Link`/`Style`/`Script`. Until now those routed
  through `eval`, which apps do not have; now they are plain DOM writes
  the host performs. The default host refuses `script` unless the mount
  granted eval.
- `history`: dioxus-history's `History` trait over abstract routes, with
  host-initiated moves delivered as a `stream<string>` from `changes()`.
  The host chooses the encoding (`MountOptions.history`: `"memory"` by
  default, `"fragment"` for a host that does not own the path); the guest
  never sees it, so dioxus-router works unchanged.

And one mechanism over all of them: `MountOptions.intercept` lets the
embedder wrap any operation of any host-implemented import table with
`(next, ...args) => result` — observe, rewrite arguments, refuse, or
replace. Policy stays out of the WIT, which remains one fixed protocol per
capability. Rules in host/src/intercept.ts: typed per operation (a sync
import cannot be given a Promise-returning interceptor), one denial
spelling per return type (`false`, `undefined`, or `eval`'s new `denied`
case, which the guest maps to `EvalError::Unsupported`), the dispatch-gate
bracket sits outside the interceptor, throwing is a host bug, and
interceptors never grant a capability the mount did not.

The eval-probe fixture becomes platform-probe and also exercises head
elements and a two-route router, including a host-driven navigation.
@lannbot
lannbot enabled auto-merge September 5, 2026 13:00
@lannbot
lannbot merged commit 17292d3 into main Sep 5, 2026
1 check passed
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.

2 participants