Skip to content

Datastar 1.0.4: vendor + patch anchors, @query, CSP mode, declaration-order hoist, StarElements leg - #85

Open
banditburai wants to merge 18 commits into
mainfrom
chore/datastar-1.0.2
Open

banditburai wants to merge 18 commits into
mainfrom
chore/datastar-1.0.2

Conversation

@banditburai

Copy link
Copy Markdown
Owner

Summary

Vendors Datastar 1.0.4 (from the pre-1.0.2 bundle on main) with durable stable-landmark patch anchors, and carries the follow-ups that the 1.0.2 → 1.0.4 changelog audit surfaced.

Datastar upgrade

  • Re-vendor 1.0.2 → 1.0.4; patches (shadow-dom-scan, outside-race-fix) re-anchored on stable landmarks so minifier renames stop breaking them (patches/patch_definitions.py).
  • @query() support; viewTransitionSelector for patch-elements (1.0.2 #1154); view-transition semantics documented (1.0.3 dropped the document fallback, so view_transition_selector= yields no transition on Firefox/WebKit).
  • star_app(csp=True): per-response nonces, Content-Security-Policy header, data-nonce for Datastar's CSP mode; end-to-end browser test with no unsafe-eval.

Fixes found on the way

  • process_datastar_kwargs hoists data-signals* (then data-computed*) ahead of same-element readers: Datastar applies attributes in markup order and an early read created the signal as "" (2b7d83e).
  • datastar:scan listener no longer registers a host as a root before Datastar's deferred first scan; a StarElements host on the page previously left the whole document unscanned (7d4a3ea).
  • starhtml.lint.check_signal_order(ft) reports a $x read on an earlier element than its declaration; Signal docs the ifmissing=False reset form.
  • devtools reads data-signals__ifmissing from its host (StarElements now declares host signals ifmissing).

Tests

  • tests/browser/test_starelements.py: StarElements leg (first-scan starvation light/shadow, scope-children rescope, Local bind/ref namespacing + __root, CSP without unsafe-eval, late host + codec fallback + declared events). Run with uv run --with playwright --with-editable ../starelements --with-editable . pytest tests/browser/test_starelements.py (see the module docstring for why both editables).
  • uv run pytest tests/unit tests/integration → 1747 passed with starelements installed (the test_devtools_* cases skip-fail without it); tests/browser → 58 passed, 1 xfailed (Chromium); the migration file also passes on Firefox/WebKit.

Design records (docs/ is gitignored; force-added): docs/designs/2026-09-21-datastar-1.0.4-integration.md, docs/designs/2026-09-21-rocket-vs-starelements.md.

Companion StarElements runtime changes (separate repo, not required to merge this) implement the Rocket adoption list: data-scope-children, attribute-aware $$ rewrite, nonced-script compile, named errors + codec fallback, manifest.

🤖 Generated with Claude Code

…nchors

Refactor patch_definitions.py to capture volatile minified tokens (scan-fn
name, event consts, handler vars, action export alias) from stable string/
structural landmarks, then fill «token» placeholders in the search/replace
operations. The same definitions now patch 1.0.1 and 1.0.2 unchanged — the
bump required no manual anchor edits.

Verified: rebuilding the prior 1.0.1 upstream with the refactor reproduces the
old datastar-core.js byte-for-byte (no behavior change); 1.0.2 applies clean;
node --check passes on core + wrapper + 14 plugins; browser suite 42 passed/6
skipped (incl. shadow-DOM scan + outside-click race); unit suite 1458 passed.

Handles the 1.0.2 c<->l handler-variable swap automatically via positional
capture. Built core/wrapper JS remain gitignored (rebuilt by bun run build).
…r 1.0.2 #1154)

Add view_transition_selector to format_element_event() and elements(); it emits
a viewTransitionSelector data line, validated with the same selector check, and
threads through the elements payload tuple + process_sse_item unpacking.

Scoped to the SSE path: the header-based Fragment path is left unchanged because
Datastar reads only selector/mode/namespace/useViewTransition from response
headers — viewTransitionSelector has no header form upstream.
- patch_definitions.py: trim module docstring to the why + guillemet rationale;
  fold count check into a walrus in apply_patch
- realtime.py: extract _warn_if_unsafe_selector so the selector and
  viewTransitionSelector paths share one validation check
- drop a redundant what-comment in the SSE ordering test
…e + retry-semantics coverage

Bundle: patches/datastar-upstream.js -> v1.0.4 (subsumes 1.0.3). Both patches re-anchored
because 1.0.4 aliases `document` to a minified local: shadow-dom-scan and outside-race-fix
now capture the alias («doc») and the outside-handler param («arg») from their landmarks;
the definitions apply to 1.0.2 and 1.0.4 alike (regression-checked). DATASTAR_VERSION 1.0.4+starhtml.

@query() (1.0.4): `query()` action in the DSL; `query` in all_meths so `@rt(..., methods="query")`
and `app.query(...)` route it (default routes stay GET/POST); QUERY is body-bearing for form
fallback and Datastar-signal extraction (_BODY_METHODS); handle_request forwards QUERY bodies.

Retry semantics (1.0.3, #1174): upstream now rebuilds HTTP-status/network/GET/form/submitter
retries from current state; the five migration tests are renamed *_rebuilds_* and assert that
for vanilla and StarHTML runtimes.

CSP mode (1.0.3/1.0.4): browser test proves the vendored bundle compiles expressions under a
strict CSP with a nonce on <html>; app-level per-request nonce plumbing is a follow-up.

Browser matrix: page fixture honours STARHTML_BROWSER=firefox|webkit; migration suite (42)
passes on Chromium, Firefox and WebKit. Per-changelog-item decisions (incl. Rocket vs
StarElements investigation) in docs/designs/2026-09-21-datastar-1.0.4-integration.md.
…ntics, un-hang compat suite

Review fixes: patch-introduced locals are `_`-prefixed with a `reserved` collision guard in
_resolve_captures (a minifier reusing u/f/g/h/d would have produced a SyntaxError that verify()
could not see); `arg` capture backreferences its own name; Client.query shortcut; WASM
handle_request forwards bodies for every _BODY_METHODS entry; README retry section rewritten
for 1.0.3's current-payload retries; the five *_rebuilds_* tests use retryInterval 400ms so the
signal patch reliably lands before the retry.

View transitions (1.0.3 #1181): viewTransitionSelector now targets the element's scoped
startViewTransition with no document fallback, so on engines without scoped view transitions
(Firefox, WebKit) a selector means no transition. realtime.py docstrings say so; new browser test
test_patch_elements_view_transition_target[document|selector] spies both entry points and passes
on Chromium/Firefox/WebKit.

tests/browser/test_browser_compatibility.py hung forever: the class-scoped async fixture and the
function-scoped tests ran on different pytest-asyncio (1.1) event loops, so Playwright awaits never
resolved. loop_scope="class" on both fixes it; the suite then exposed page.touch_screen (fixed to
touchscreen) and a scroll test that can never pass (harness inlines the ES-module plugin as a
classic script, never loads Datastar) -> strict xfail with reason. Suite: 7 pass, 1 xfail.
… CSP mode; Rocket vs StarElements study

csp=True (or a policy template containing {nonce}) makes every HTML response carry a fresh
128-bit nonce: data-nonce on <html> (Datastar reads it once and compiles expressions through
nonced <script>s instead of Function()), nonce on every rendered <script>/<style> (import map,
Datastar loader, plugin modules, theme, devtools, the handler's own tags; explicit nonces are
kept), and a Content-Security-Policy header, default
  script-src 'nonce-{nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'self'
('strict-dynamic' lets the nonced module scripts import datastar-core.js, plugin chunks and CDN
deps without host allow-lists). csp_nonce(req) exposes the value; exception pages get one too;
JSON/plain responses are untouched. Not covered by nonces, documented: Datastar's inline style=
writes (policy constrains scripts only), raw NotStr HTML, and StarElements' new Function.

Tests: tests/unit/test_csp_nonce.py (9) and test_star_app_csp_mode_end_to_end, which proxies the
real ASGI app through Playwright so the browser enforces StarHTML's own header; passes on
Chromium, Firefox and WebKit with zero CSP violations.

docs/designs/2026-09-21-rocket-vs-starelements.md: source-level comparison of the new free
Rocket bundle with StarElements; seven adoption items (data-scope-children, attribute-aware $$
rewrite, pre-upgrade data-ignore guard, CSP-safe setup compile, named errors, manifests, nested
paths), what to keep, what is not applicable, and the risk to the shadow-dom-scan patch.
… readers

Datastar evaluates an element's attributes in markup order; a reader (data-text,
data-attr, data-bind, ...) that runs before its declaration auto-creates the signal
as '' and __ifmissing then keeps it. process_datastar_kwargs now returns
declarations first so every emit path renders them ahead of readers.
… before the deferred first scan

Datastar defers its first scan to a timeout and then scans roots.size ? [...roots]
: [documentElement]. A StarElements host connecting before that timeout dispatched
datastar:scan, the patched listener added the host to roots, and the document was
never scanned (no datastar-ready, page-level data-* dead). Light-DOM hosts are now
left to the pending document scan; shadow roots are scanned once datastar-ready
fires. tests/browser/test_starelements.py: StarElements leg (run with both
editables, see docstring).
…clares signals ifmissing now); tests/browser/test_starelements: CSP leg (star_app(csp=True), setup+static scripts, no unsafe-eval)
…tion); Signal docs the ifmissing=False reset form; hoist orders data-signals before data-computed

- starhtml.lint.check_signal_order(ft) walks a rendered tree in document order
  and reports $x reads on an element before the one declaring x (data-signals:x,
  data-signals object keys, data-computed:x). Undeclared reads are not reported
  (layout/persist/patch may own them); $$locals are ignored.
- Signal docstring: ifmissing=False emits a plain data-signals object (server-
  authoritative reset), signals(name=value) resets from a handler.
- _declarations_first: data-signals* ahead of data-computed* ahead of readers.
…trs keep declarations first; deferred shadow scan skips disconnected hosts; stronger StarElements browser tests

- lint: $user.name reads the root signal; bare data-bind/ref/indicator values
  are reads (an undeclared data_bind ahead of the declaration was missed); only
  depth-1 object keys declare; string literals masked; data-signals string values
  are literals (StarHTML escapes $).
- html._apply_slot_attrs_to_children: slot dicts merged after a child's readers
  now go through _declarations_first.
- shadow-dom-scan patch: the datastar-ready-deferred scan checks host.isConnected
  (roots are never removed from Datastar's Set).
- Signal docstring: ifmissing=False is the reset form only when declared.
- tests/browser/test_starelements: assertions no longer satisfied by SSR
  fallback text; datastar-ready counted; new tests for __root across patches +
  nested hosts, re-patched outer markup, empty int default registration.
…scoped (7 shapes); design record: patch modes + apply() export investigation
…rElements defines elements after it

Rocket queues element definitions until Datastar's document observer is active;
StarElements defined at DOMContentLoaded, which is the root cause of the
first-scan starvation. The wrapper registers its datastar-ready listener before
the first-scan timeout can fire (Datastar has no 'already fired?' query), and
StarElements now waits on it. With the original naive datastar:scan listener all
16 StarElements browser tests pass, so the guarded listener is defence in depth.
…ener; wrapper ready promise removed; StarElements browser tests for the final boot/scan contract

Light-DOM hosts are covered by Datastar's document observer; shadow roots are
bound by StarElements calling apply(shadowRoot, true) after Datastar's first
scan (ordered by a macrotask after its static import), so the listener's
ready/ShadowRoot deferral is gone. New tests: multi-host selector not
cross-wired, inserted host with $$ children scoped before apply, <pre> newline
through the pre-scope, light hosts never enter observedRoots (late attribute()
applies once per element).

This branch has not been deployed

No deployments
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