This is a pre-release. djust 1.0.0 has shipped since: read the djust 1.0.0 release notes.
Before you upgrade, read the upgrade guide.
Added
MoveSubtreeVDOM patch — matched{% if %}boundaries can now be repositioned (#1666). When adj-ifconditional boundary is present in both the old and new render but its position among the parent's significant children shifts (siblings added/removed/reordered around it), the markers — id-less#commentnodes that a plainMoveChildcan't target — previously stayed anchored at their old position, so the conditional rendered in the wrong place (or the round-trip diverged). The differ now emitsMoveSubtree { id, path, d, index }(the "move" verb for boundary spans, completing the Remove/Insert/Move trio), and the client (12-vdom-patch.js) locates the<!--dj-if id="X"-->...<!--/dj-if-->marker pair by id, detaches the whole range, and re-inserts it at the target index — preserving inner node identity (and any state/focus tied to inner dj-ids), unlike a Remove+Insert. Applied in a new phase after the path/index child ops so the surrounding siblings have settled. A client-faithful differential harness measured the matched-boundary-reposition residual drop from ~22 to ~6 per 6000 adversarial re-renders. Wire shape pinned inwire_protocol_snapshot.rs; behavior pinned bymatched_djif_boundary_repositioned_via_move_subtree(Rust) andtests/js/move_subtree_patch.test.js(client).LiveViewTestClient.assert_allowlisted()+assert_all_routed_liveviews_allowlisted()— make the allowlist gap testable (#1674). A URL-routed LiveView forgotten fromLIVEVIEW_ALLOWED_MODULEShas its WebSocket mount rejected and silently degrades to full-page HTTP re-renders — butmount()instantiates the view class directly, bypassing the allowlist, so the misconfiguration was invisible to the unit suite.client.assert_allowlisted()fails fast for one view; the standaloneassert_all_routed_liveviews_allowlisted()walks the root URLconf and guards every routed view in a single test. Both mirror the runtime enforcement exactly (non-empty allowlist, prefix match) and are no-ops when the allowlist is unset/empty.
Fixed
VDOM
InsertChild.ref_dno longer mis-inserts under sibling reorder. The differ populatedref_dwith a positional guess (old.get(new_index).djust_id) — the dj-id of whatever old node happened to sit at the new node's index. The client (12-vdom-patch.js) honorsref_d(it doesinsertBefore(node, querySelector(':scope > [dj-id=ref_d]'))and only falls back to the index whenref_dis absent), so under a reorder that guess is the wrong reference and the new node lands in the wrong position. The differ now emitsref_d: Noneand relies on the index:InsertChildis applied last in the client's phase order (afterRemoveSubtree/InsertSubtree/RemoveChild/MoveChild), so the index is resolved against the settled new-frame and is reliable. A client-faithful differential harness measured ~18 mis-inserts per 6000 adversarial re-renders before the fix, 0 after. The#1408invariant is preserved (aref_d, when present, must resolve in the OLD tree). Regression-pinned bykeyed_insert_ref_d_is_safe_not_a_wrong_guessincrates/djust_vdom/tests/test_diff_robustness_gaps.rs.Custom template filter that
mark_safe()s its output at runtime is no longer HTML-escaped (#1660). The Rust renderer decided auto-escaping purely by filter name (the staticis_safe=Trueflag / the built-insafe_output_filterslist), never by the filter result's runtimeSafeString-ness — unlike Django'srender_value_in_context, which escapes iff the final value lacks__html__. So a filter like@register.filter def md(v): return mark_safe(...)(nois_safe=True) had its HTML escaped on both the HTTP-prerender and WS-mount paths.apply_custom_filternow reports whether the Python result carries__html__;apply_filter_full_safethreads that out as a per-filter runtime-safe flag; and the renderer (Variable + InlineIf arms) honours the last filter's runtime safeness in its escape decision (additive — a later plain-returning filter re-taints, matching Django). Security-hardened: the runtime-safe marker is honoured only for a genuinestrsubclass with__html__(a realSafeString) — Django stringifies any non-strvalue before the__html__check, and djust'sValueextraction stringifies arbitrary objects via__str__, so trusting__html__alone would let a non-strobject's attacker-controlled__str__reach output unescaped (XSS). Workaround no longer needed:@register.filter(is_safe=True)and pre-render+|safestill work. Pinned by the runtime-SafeString matrix (text/attribute context,x|md|upperre-taint,x|upper|md, plain-filter-still-escaped) and the non-str-__html__impostor XSS guard intests/unit/test_rust_custom_filters_1121.py; gate-off self-test (Action #1200/#1468) + a 5-lens adversarial XSS verification confirmed non-tautological. The parallel{% firstof %}/{% cycle %}get_valuepipe path still over-escapes a runtime-safe value (fail-safe, not XSS) — tracked in #1672.Unallowlisted URL-routed LiveView no longer degrades silently — actionable signal at ship-time and runtime (#1674). A URL-routed LiveView whose module is missing from
LIVEVIEW_ALLOWED_MODULEShas its WebSocket mount correctly rejected, then silently falls back to full-page HTTP re-renders that look like the app works (events fire, DB updates, page re-renders) — a DX trap, not a security/correctness bug. Two fixes: (1) thedjust.V005system check now also discovers URL-routed views by walking the root URLconf (_routed_liveview_classes), not only__subclasses__()(which is import-timing dependent), so the misconfiguration is caught atmanage.py check/ship-time; V005's matching was aligned with the WebSocket mount enforcement (websocket.py) — prefix match, enforced only when the allowlist is non-empty — fixing a pre-existing false positive where an empty[]flagged every view and a prefix allowlist (['myapp']) wrongly flaggedmyapp.views(parallel-path-drift, CLAUDE.md #1646). (2) The client's HTTP-fallback notice (11-event-handler.js), previously adjustDebug-gatedconsole.log(silent by default), is now a once-per-session, un-gatedconsole.warnpointing the developer atLIVEVIEW_ALLOWED_MODULES. Pinned bypython/djust/tests/test_v005_routed_allowlist_1674.py,test_assert_allowlisted_1674.py, andtests/js/http_fallback_warning_1674.test.js; dogfooded against the demo (1 real finding, no flood).
Tests
- Apply-level regression pin for the #1636
{% if %}1/Npatch failure.#1636'sInsertChild1/N patches failed(a false→true{% if %}add) was a manifestation of #1640 (the index resolvergetSignificantChildrenand the path walkergetNodeByPathdisagreeing on whether a regular HTML comment counts) and was confirmed fixed on rc14 by the consumer. #1640 shipped a predicate-level test (in thesignificant_children_comment_filter_1640suite); this adds the missing applied-patch-level pin for the chronically-reopened if-block cluster (#1358/#1408/#1550/#1552/#1555/#1636): anInsertChildwhose index points past a regular<!-- comment -->lands between the right significant siblings, not one slot early (the1/N). Gate-off proof (Action #1200/#1468): flippingisSignificantChildback to counting all comments and rebuilding makes the mid-insert case fail (the inserted node lands at significant index 0 instead of 1). 3 regression cases intests/js/insert_child_regular_comment_1636.test.js. No source change — guards an already-shipped fix.