v0.5.0
Goodbye, Scope
This long-awaited release changes the nature of the reactive system by removing the entire concept of explicit Scope
and the ubiquitous cx
variable.
The primary impetus behind this change is that it increases the correctness of the behavior of the reactive system, and fixes several persistent issues.
From 0.0 to 0.4, Leptos allocated signals in a dedicated The solution to this problem was to do what I should have been doing a year ago, and merge the memory allocation function of Essentially, rather than being owned by a Click here for more details.
In order to provide signals that implement `Copy` are are `'static` and are therefore easy to use with closures and event listeners in 100% safe Rust, Leptos allocates memory for signals, memos, and effects in an arena. This raises the question: When is it safe to deallocate/dispose of these signals?
Scope
, which was ubiquitous in APIs. This had several drawbacks
Scope
argument on many functions prevented us from implementing many traits that could not take an additional argument on signals, like From
, Serialize
/Deserialize
.
Scope
was stored in a variable that was passed around, meaning that the “wrong” scope could be passed into functions (most frequently Resource::read()
). If, for example, a derived signal or memo read from a resource in the component body, and was called under a Suspense
lower in the tree, the Scope
used would be from the parent component, not the Suspense
. This was just wrong, but involved wrapping the function in another closure to pass in the correct Scope
.
Scope
s were manually created and disposed, or in which on_cleanup
was never called. (See #802 and #918 for more background.)
Scope
into the reactive graph itself, which already handles reactive unsubscriptions and cleanup. JavaScript doesn’t deal with memory management, but SolidJS handles its onCleanup
through a concept of reactive ownership; disposing of memory for our signals is really just a case of cleanup on an effect or memo rerunning.
Scope
every signal, effect, or memo is now owned by its parent effect or memo. (If it’s in an untrack
, there’s no reactive observer but the reactive owner remains.) Every time an effect or memo reruns, it disposes of everything “beneath” it in the tree. This makes sense: for a signal to be owned by an effect/memo, it must have been created during the previous run, and will be recreated as needed during the next run, so this is the perfect time to dispose of it.
It also has the fairly large benefit of removing the need to pass cx
or Scope
variables around at all, and allowing the implementation of a bunch of different traits on the various signal types.
Now that we don't need an extra Scope
argument to construct them, many of the signal types now implement Serialize
/Deserialize
directly, as well as From<T>
. This should make it significantly easier to do things like "reactively serialize a nested data structure in a create_effect
" — this removed literally dozens of lines of serialization/deserialization logic and a custom DTO from the todomvc
example. Serializing a signal simply serializes its value, in a reactive way; deserializing into a signal creates a new signal containing that deserialized value.
Migration is fairly easy. 95% of apps will migrate completely by making the following string replacements:
cx: Scope,
=> (empty string)cx: Scope
=> (empty string)cx,
=> (empty string)(cx)
=>()
|cx|
=>||
Scope,
=> (empty string)Scope
=> (empty string) as needed- You may have some
|_, _|
that become|_|
or|_|
that become||
, particularly for thefallback
props on<Show/>
and<ErrorBoundary/>
.
Basically, there is no longer a Scope
type, and anything that used to take it can simply be deleted.
For the 5%: if you were doing tricky things like storing a Scope
somewhere in a struct or variable and then reusing it, you should be able to achieve the same result by storing Owner::current()
somewhere and then later using it in with_owner(owner, move || { /* ... */ })
. If you have issues with this kind of migration, please let me know by opening an issue or discussion thread and we can work through the migration.
Islands
This release contains an initial, but very functional, implementation of the “islands architecture” for Leptos.
This adds an experimental-islands
feature that opts you into a rendering mode that's different from the traditional server-rendered/client-hydrated model described here, in which the entire page requested is rendered to HTML on the server for the first request, and then runs on the client to hydrate this server-rendered HTML, and subsequent navigations take place by rendering pages in the browser.
The
experimental-
inexperimental-islands
means “parts of this API may need to change, and I won’t guarantee that its APIs won’t break during 0.5,” but I have no reasons to believe there are significant issues, and I have no planned API changes at present.
With this islands feature on, components are only rendered on the server, by default. Navigations follow the traditional/multi-page-app (MPA) style of navigating by fetching a new HTML page from the server. The name "islands" comes from the concept of the "islands architecture," in which you opt into small “islands” of interactivity in an “ocean” of server-rendered, non-interactive HTML.
This allows reducing the WASM binary size by a lot (say ~80% for a typical demo app), making the time-to-interactive for a page load much faster. It also allows you to treat most components as “server components” and use server-only APIs, because those plain components will never need to be rendered in the browser.
I still need to write some guide-style docs for the book, but I tried to put a pretty good amount of information in the PR, which you should read if you’re interested in this topic or in trying out islands.
There’s significant additional exploration that will take place in this direction. Expect a longer treatment in an upcoming updated roadmap posted as a pinned issue.
Additional Reading
- Jason Miller, “Islands Architecture”, Jason Miller
- Ryan Carniato, “Islands & Server Components & Resumability, Oh My!”
- “Islands Architectures” on patterns.dev
- Astro Islands
Static Site Generation
This release includes some preliminary work on building static site rendering directly into the framework, mostly as part of leptos_router
and the server integrations. Static site generation is built off of two key components, a new <StaticRoute />
component and the leptos_router::build_static_routes
function.
StaticRoute
defines a new static route. In order to be a static route, all parent routes must be static in order to ensure that complete URLs can be built as static pages. A “static route” means that a page can be rendered as a complete HTML page that is then cached and served on subsequent requests, rather than dynamically rendered, significantly reducing the server’s workload per request.
StaticRoute
takes a path and view (like any Route
) and a static_params
prop. This is a function that returns a Future
, which is used to provide a map of all the possible values for each route param. These static params are generated on the server. This means you can do something like call a #[server]
function or access a database or the server filesystem. For example, if you have a /post/:id
path, you might query the database for a list of all blog posts and use that to generate a set of possible params.
StaticRoute
can be given an optional mode: StaticMode::Upfront
(the default) or StaticMode::Incremental
. Upfront
means that all given routes will be generated when you call build_static_routes
, and a 404 will be returned for any other pages. Incremental
means that all the options you give in static_params
will be generated up front, and additional pages will be generated when first requested and then cached.
Where our routes are defined, we can include a StaticRoute
:
view! {
<Routes>
<StaticRoute
mode=StaticMode::Incremental
path="/incr/:id"
view=StaticIdView
static_params=move || Box::pin(async move {
let mut map = StaticParamsMap::default();
map.insert("id".to_string(), vec![(1).to_string(), (2).to_string()]);
map
})
/>
</Routes>
}
In main.rs
, we build the static routes for the site on server startup:
let (routes, static_data_map) = generate_route_list_with_ssg(App);
build_static_routes(&conf.leptos_options, App, &routes, &static_data_map)
.await
.unwrap();
More to Come
There is additional work needed here as far as providing examples, and building out the story for things like invalidation (when to rebuild a page) and build hooks (when to build additional pages).
Other New Features in 0.5
attr:
on components, and spreading attributes
Makes it much easier to pass some set of attributes to be given to a component (with attr:
passed into a #[prop(attrs)]
prop), and then to spread them onto an element with {..attrs}
syntax.
#[component]
pub fn App() -> impl IntoView {
view! {
<Input attr:value="hello" attr:label="foo" />
<Input attr:type="number" attr:value="0" />
}
}
#[component]
pub fn Input(
#[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
) -> impl IntoView {
view! {
<input {..attrs} />
<pre>{format!("{attrs2:#?}")}</pre>
}
}
Generics on components in view
Newly added support for syntax that identifies a generic type for a component in the view
#[component]
pub fn GenericComponent<S>(#[prop(optional)] ty: PhantomData<S>) -> impl IntoView {
std::any::type_name::<S>()
}
#[component]
pub fn App() -> impl IntoView {
view! {
<GenericComponent<String>/>
<GenericComponent<usize>/>
<GenericComponent<i32>/>
}
}
Callback
types
This release includes a Callback
type that makes it much easier to pass optional callbacks as component props, which was previously not possible with generically-typed callbacks.
#[component]
fn MyComponent(
#[prop(optional, into)] render_number: Option<Callback<(i32, i32), String>>,
) -> impl IntoView {
render_number.map(|render_nummber| {
view! {
<div>
// function call syntax works on nightly
{render_number((42, 37))}
// does the same thing
{render_number.call((42, 37))}
</div>
}
})
}
#[component]
pub fn App() -> impl IntoView {
view! {
<MyComponent render_number=|(x, y)| format!("x: {x} - y: {y}")/>
}
}
with!()
and update!()
macros
Nested .with()
calls are a pain. Now you can do
let (first, _) = create_signal("Bob".to_string());
let (middle, _) = create_signal("J.".to_string());
let (last, _) = create_signal("Smith".to_string());
let name = move || with!(|first, middle, last| format!("{first} {middle} {last}"));
instead of
let name = move || {
first.with(|first| {
middle.with(|middle| last.with(|last| format!("{first} {middle} {last}")))
})
};
Rustier interfaces for signal types
This framework's origins as a Rust port of SolidJS mean we've inherited some functional-JS-isms. Combined with the need to pass cx
everywhere prior to 0.5 this has tended to mean we've gone with create_
and so on rather than Rusty ::new()
. This simply adds a few Rustier constructors like
let count = RwSignal::new(0);
let double_count = Memo::new(move |_| count() * 2);
Optional #[server]
Arguments
All arguments to #[server]
are now optional: change default prefix to /api
and default to generating a PascalCase
type name from the function name
// before
#[server(MyName, "/api")]
pub async fn my_name() /* ... */
// after
#[server]
pub async fn my_name /* ... */
JS Fetch API integration support
We have adapted the Axum integration to be usable when running inside JS-hosted WASM environments (Deno Deploy, Cloudflare Workers, etc.) This can be done by disabling default-features
on axum
and leptos_axum
, using the axum-js-fetch
library, and enabling the wasm
feature on leptos_axum
.
leptos_axum = { version = "0.5", default-features = false, optional = true }
axum = { version = "0.6", default-features = false, optional = true }
axum-js-fetch = { version = "0.2", optional = true }
# ...
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:axum-js-fetch",
"leptos_axum/wasm",
# ...
]
There's a full Hackernews example in the repo that runs within Deno.
Feature Grab-Bag
- Updated
Resource
APIs. Resources now follow the same API as other signals:.get()
and.with()
apply to the current value (which may beNone
). This means the old.with()
, which was only called when the resource had resolved, is now.map()
. There's also a new.and_then()
method which can be called to deeply map over resources that returnResult
, like server functions. The goal of these methods is to reduce the amount of boilerplate mapping necessary with resources previously. Check out theResource
docs for more info. create_effect
now returns anEffect
struct. This exists mostly so you can.dispose()
it early if you need. (This may require adding some missing semicolons, ascreate_effect
no longer returns()
.)create_effect
’s first run is now scheduled on the next tick, usingqueueMicrotask
. In other words,create_effect
runs after your component returns a view, not immediately as the component runs. This removes most of the cases in which nestingrequest_animation_frame
inside acreate_effect
had been necessary.- Support passing signals directly as attributes, classes, styles, and props on stable.
- Many APIs that previously took
impl Into<Cow<'static, str>>
now takeimpl Into<Oco<'static, str>>
.Oco
(Owned Clones Once) is a new type designed to minimized the cost of cloning immutable string types, like the ones used in theView
. Essentially this makes it cheaper to clone text nodes and attribute nodes within the renderer, without imposing an additional cost when you don't need to. This shouldn't require changes to your application in most cases of normal usage. (See #1480 for additional discussion.)
Minor Breaking Changes
leptos_axum::generate_route_list()
This function is no longer async
. This means removing a single .await
from main.rs
in any Leptos-Axum app, but brings the API of the Axum (and Viz) integrations into line with the Actix one, and unlocks the use of this function in some non-async settings where it was desired.
<For/>
prop view
renamed children
The name of the function that renders each row of data to a view is now children
rather than view
. This means that the view can now be included inline, without needing to break out of the markup into another closure and view!
macro invocation.
In other words,
let (data, set_data) = create_signal(vec![0, 1, 2]);
view! {
<For
each=data
key=|n| *n
// stores the item in each row in a variable named `data`
let:data
>
<p>{data}</p>
</For>
}
is the same as
let (data, set_data) = create_signal(vec![0, 1, 2]);
view! {
<For
each=data
key=|n| *n
children=|data| view! { <p>{data}</p> }
/>
}
which replaces
let (data, set_data) = create_signal(vec![0, 1, 2]);
view! {
<For
each=data
key=|n| *n
view=|data| view! { <p>{data}</p> }
/>
}
use_navigate
navigate function no longer returns a value
The navigate("path", options)
call now uses request_animation_frame
internally to delay a tick before navigating, which solves a few odd edge cases having to do with redirecting immediately and the timing of reactive system cleanups. This was a change that arose during 0.3 and 0.4 and is being made now to coincide with other breaking changes and a new version.
If you were relying on the Result<_, _>
here and want access to it, let me know and we can bring back a version that has it. Otherwise, this shouldn't really require any changes to your app.
window_event_listener
and friends now return a handle for removal
This one was just an API oversight originally, again taking advantage of the semver update. window_event_listener
and its untyped version now return a WindowListenerHandle
with a .remove()
method that can be called explicitly to remove that event, for example in an on_cleanup
.
#[component]
fn TypingPage() -> impl IntoView {
let handle = window_event_listener(ev::keypress, |ev| {
/* do something */
});
on_cleanup(move || handle.remove());
}
Other Breaking Changes
use_context
no longer works inside event listeners, insidespawn_local
, etc. Useuse_context
inside the body of a component and move the context data into those other closures.bind:
has been renamedlet:
, which makes more sense for variable binding. (This applies to components likeAwait
and nowFor
, which take arguments in theirchildren
.)Transition
’sset_pending
prop now applies#[prop(into)]
, which means it can take any signal setter. If you were doing something already likeset_pending=something.into()
the compiler will now get confused; just remove that.into()
.create_effect
’s first run is now scheduled on the next tick, usingqueueMicrotask
. In other words,create_effect
runs after your component returns a view, not immediately as the component runs. (Listing this as both a feature and a breaking change, as it is a behavior change!)- The
log!
,warn!
, anderror!
macros have been moved into a separatelogging
module to avoid naming conflicts with thelog
andtracing
crates. You can import them asuse leptos::logging::*;
if desired. - Signal traits now take an associated
Value
type rather than a generic (see #1578) - Changes to
Resource
APIs: the function formerly known as.with()
is now.map()
, and.read()
is now.get()
to match other signals.
What's Changed
- change: shift from
Scope
-based ownership to reactive ownership by @gbj in #918 - refactor(workflows): extract calls by @agilarity in #1566
- refactor(verify-changed-examples): improve readability and runtime by @agilarity in #1556
- Remove Clone requirement for slots in Vec by @Senzaki in #1564
- fix: INFO is too high a level for this prop tracing by @gbj in #1570
- fix: suppress warning about non-reactivity when calling
.refetch()
(occurs in e.g., async blocks in actions) by @gbj in #1576 - fix: nightly warning in server macro for lifetime by @markcatley in #1580
- fix: runtime disposal time in
render_to_string_async
by @gbj in #1574 - feat: support passing signals directly as attributes, classes, styles, and props on stable by @gbj in #1577
- feat: make struct name and path optional for server functions by @gbj in #1573
- support effect dispose by @flisky in #1571
- fix(counters_stable): restore wasm tests (#1581) by @agilarity in #1582
- fix: fourth argument to server functions by @gbj in #1585
- feat: signal traits should take associated types instead of generics by @gbj in #1578
- refactor(check-stable): use matrix (#1543) by @agilarity in #1583
- feat: add
Fn
traits for resources on nightly by @gbj in #1587 - Convenient event handling with slots by @rkuklik in #1444
- fix: correct logic for resource loading signal when read outside suspense (closes #1457) by @gbj in #1586
- Update autoreload websocket connection to work outside of localhost by @rabidpug in #1548
- Some resource and transition fixes by @gbj in #1588
- fix: broken test with untrack in tracing props by @gbj in #1593
- feat: update to
typed-builder
0.16 (closes #1455) by @gbj in #1590 Oco
(Owned Clones Once) smart pointer by @DanikVitek in #1480- docs: add docs for builder syntax by @gbj in #1603
- chore(examples): improve cucumber support #1598 by @agilarity in #1599
- fix(ci): add new webkit dependency (#1607) by @agilarity in #1608
- fix(macro/params): clippy warning by @Maneren in #1612
- Improve server function client side error handling by @drdo in #1597
- Don't try to parse as JSON the result from a server function redirect by @JonRCahill in #1604
- fix: add docs on #[server] functions by @realeinherjar in #1610
- doc(examples): add fantoccini to test-runner-report (#1615) by @agilarity in #1616
- docs: Derived signals - Clarified derived signals by @martinfrances107 in #1614
- doc(book,deployment): update reference to binary in dockerfile by @SadraMoh in #1617
- docs: fix typo by @dpytaylo in #1618
- docs: remove extra space by @Lawqup in #1622
- docs(book): fix wrong variable name by @Gaareth in #1623
- Callback proposal by @rambip in #1596
- Configuration for Hot-Reloading Websocket Protocol and enable ENV PROD selection by @Indrazar in #1613
- feat: implement simple spread attributes by @mrvillage in #1619
- fix: memoize Suspense readiness to avoid rerendering children/fallback by @gbj in #1642
- feat: add component generics by @mrvillage in #1636
- hide
get_property
by @jquesada2016 in #1638 - Into thing boxed by @jquesada2016 in #1639
- docs: cleanup by @Banzobotic in #1626
- fix: versioned resources never decrement Suspense (closes #1640) by @gbj in #1641
- Rename into signal traits by @jquesada2016 in #1637
- feat: start adding some Rustier interfaces for reactive types by @gbj in #1579
- test(error_boundary): add e2e testing by @agilarity in #1651
- fix: custom events on components by @liquidnya in #1648
- fix: compare path components to detect active link in router by @flo-at in #1656
- Tailwind example update by @SleeplessOne1917 in #1625
- refactor(examples): extract client process tasks (#1665) by @agilarity in #1666
- change: move logging macros into a
logging
module to avoid name conflicts withlog
andtracing
by @gbj in #1658 - Router version bump by @martinfrances107 in #1673
- Chore: Bump to actions/checkout@v4 by @martinfrances107 in #1672
Rc
backedChildrenFn
by @Baptistemontan in #1669- Remove (most) syn 1 dependencies by @blorbb in #1670
- chore: Removed resolver link warning. by @martinfrances107 in #1677
- examples: add note about potential for memory leaks with nested signals by @gbj in #1675
- feat: islands by @gbj in #1660
- Docs: a bunch of small improvements by @gbj in #1681
- Chore: Remove ambiguity surrounding version numbers. by @martinfrances107 in #1685
- fix: restore deleted
extract_with_state
function by @gbj in #1683 - fix: broken
mount_to_body
in CSR mode by @gbj in #1688 - Chore: cleared "cargo doc" issue. by @martinfrances107 in #1687
- Update interlude_projecting_children.md by @mjarvis9541 in #1690
- feat: Add dynamically resolved attributes by @mrvillage in #1628
- docs: add docs for
#[island]
macro by @gbj in #1691 - Feedback Wanted — change: run effects after a tick by @gbj in #1680
- with! macros by @blorbb in #1693
- build(examples): make it easier to run examples by @agilarity in #1697
- A few fixes to rc1 by @gbj in #1704
- fix: impl
IntoView
forRc<dyn Fn() -> impl IntoView>
by @Baptistemontan in #1698 - impl
From<HtmlElement<El>> for HtmlElement<AnyElement>
by @jquesada2016 in #1700 - doc(examples): reference run instructions by @agilarity in #1705
- Callbacks: Manual Clone and Debug impl; Visibility fix by @lpotthast in #1703
- impled LeptosRoutes for &mut ServiceConfig in integrations/actix by @cosmobrain0 in #1706
- Update possibly deprecated docs for
component
macro by @ChristopherPerry6060 in #1696 - fix: exclude markdown files from examples lists by @agilarity in #1716
- fix(examples/build): do not require stop to end trunk by @agilarity in #1713
- Fix a typo that prevented the latest appendix from appearing in the book by @g2p in #1719
- feat: support
move
onwith!
macros by @blorbb in #1717 - feat: implement
Serialize
andDeserialize
forOco<_>
by @gbj in #1720 - fix: document
#[prop(default = ...)]
as in Optional Props (closes #1710) by @gbj in #1721 - fix: correctly register
Resource::with()
(closes #1711) by @gbj in #1726 - fix: replace uses of
create_effect
internally withcreate_isomorphic_effect
(closes #1709) by @gbj in #1723 - fix: relax bounds on
LeptosRoutes
by @ChristopherPerry6060 in #1729 - feat: Allow component names to be paths by @mrvillage in #1725
- feat: use
attr:
syntax rather thanAdditionalAttributes
by @gbj in #1728 - fix(examples/error_boundary): ci error by @agilarity in #1739
- Callback clone fix by @lpotthast in #1744
- fix:
Resource::with()
pt. 2 — (closes #1742 without reopening #1711) by @gbj in #1750 - chore(server_fn_macro): improve docs by @fundon in #1733
- fix:
Resource::with()
(pt. 3!) — closes #1751 without breaking #1742 or #1711 by @gbj in #1752 - feat: make
Transition
set_pending
use#[prop(into)]
by @gbj in #1746 - 1742-2 by @gbj in #1753
- chore(server_fn): improve docs by @fundon in #1734
- feat: correctly
use_context
between islands by @gbj in #1747 - Fix Suspense issues on subsequent navigations by @gbj in #1758
- Support default values for annotated server_fn arguments by @g2p in #1762
- chore(leptos_router): improve docs by @fundon in #1769
- Better document the interaction of SsrModes with blocking resources by @g2p in #1765
- Reimplement
Oco
cloning by @DanikVitek in #1749 - feat: Static Site Generation by @mrvillage in #1649
- chore(leptos_marco): enhancement of document generation by @fundon in #1768
- chore(leptos_hot_reload): apply lints suggestions by @fundon in #1735
- fix: broken benchmarks (closes #1736) by @gbj in #1771
- Change leptos_axum generate_route_list function to not be async by @nicoburniske in #1485
- docs: error in
view!
macro if you usecx,
by @gbj in #1772 - Add tooling for nix flakes by @rambip in #1763
- change: only run
create_local_resource
in the browser by @gbj in #1777 - seanaye/feat/js fetch by @seanaye in #1554
- docs: fix
Show
docs reference to scope by @gbj in #1779 - change: use
let:
instead ofbind:
by @gbj in #1774 - chore(leptos_meta): enhance links in docs by @fundon in #1783
- Update working_with_signals.md by @SSeanin in #1785
- change: enable inline children for
For
by switching tochildren
andbind:
by @gbj in #1773 - feat(leptos_config): only enable
toml
feature for theconfig
dependency by @messense in #1788 - Scoped Futures by @jquesada2016 in #1761
- Fix render_route error message and matching of non standard routes in… by @benwis in #1799
- feat: implement
From<Fn() -> T>
forSignal<T>
by @gbj in #1801 - moved callback to leptos root and improved design by @rambip in #1795
- fix: improve rust-analyzer auto-completion by @pikaju in #1782
- chore: leptos_marco: Bumped outdated crates by @martinfrances107 in #1796
- Set Content-Type header for all Responses to text/html;charset="utf-8" by @benwis in #1803
- fix: broken
Suspense
when a resource loads immediately (closes #1805) by @gbj in #1809 - Remove extra angle brackets in generate_head_metadata by @Sirius902 in #1811
- feat: better error handling for
ScopedFuture
by @gbj in #1810
New Contributors
- @Senzaki made their first contribution in #1564
- @flisky made their first contribution in #1571
- @rkuklik made their first contribution in #1444
- @rabidpug made their first contribution in #1548
- @Maneren made their first contribution in #1612
- @drdo made their first contribution in #1597
- @JonRCahill made their first contribution in #1604
- @realeinherjar made their first contribution in #1610
- @SadraMoh made their first contribution in #1617
- @dpytaylo made their first contribution in #1618
- @Lawqup made their first contribution in #1622
- @Gaareth made their first contribution in #1623
- @rambip made their first contribution in #1596
- @mrvillage made their first contribution in #1619
- @Banzobotic made their first contribution in #1626
- @liquidnya made their first contribution in #1648
- @flo-at made their first contribution in #1656
- @Baptistemontan made their first contribution in #1669
- @blorbb made their first contribution in #1670
- @mjarvis9541 made their first contribution in #1690
- @cosmobrain0 made their first contribution in #1706
- @g2p made their first contribution in #1719
- @nicoburniske made their first contribution in #1485
- @SSeanin made their first contribution in #1785
- @messense made their first contribution in #1788
- @pikaju made their first contribution in #1782
- @Sirius902 made their first contribution in #1811
Full Changelog: v0.4.9...v0.5.0