Announcing WebURL - a new URL type for Swift

Hello Swift community!

For a while now, I've been working on a new URL type for Swift. It has taken a long time, but it's finally at a stage where I'm happy to make a first release. So let me introduce you to WebURL!


WebURL - A new URL type for Swift

WebURL is a brand-new URL library. It is written entirely in Swift, implements the latest URL standard, and has a great API designed to take full advantage of Swift's features.

The repository is at https://github.com/karwa/swift-url

To use WebURL in a SwiftPM project, add the following line to the dependencies in your Package.swift file:

.package(url: "https://github.com/karwa/swift-url", from: "0.1.0"),

And add a dependency on the "WebURL" product.

Additionally, I've developed a prototype port of async-http-client, which allows you to perform http(s) requests using WebURL, and shows how easy it can be to adopt in your library.

Note: This project isn't limited to "web"-related URLs, it also has full support for "file" URLs, "data" URLs, and custom schemes like you might use for app-internal links.

It is called 'WebURL' because a) 'WebURL' is short, b) it has 'URL' in the name, and c) is different enough from Foundation's 'URL' that you hopefully won't confuse them.

A Modern Standard

The history of URLs is messy. There have been several standards over the years, working groups were established and closed due to lack of engagement, people came up with names like "URIs", "IRIs" and even a thing called a "URN"s in order to describe their various ideas for, uhm... URLs. As IBM's Sam Ruby and Adobe's Larry Masinter wrote in an IETF working draft in 2015:

The main problem is conflicting specifications that overlap but don't match each other.
...
From a user or developer point of view, it makes no sense for there to be a proliferation of definitions of URL nor for there to be a proliferation of incompatible implementations. This shouldn't be a competitive feature.

and as Daniel Stenberg (lead developer of CURL) explained on his blog:

A “URL” given in one place is certainly not certain to be accepted or understood as a “URL” in another place.

Not even curl follows any published spec very closely these days, as we’re slowly digressing for the sake of “web compatibility”. There’s no unified URL standard and there’s no work in progress towards that.

Well, the WHATWG's URL Living Standard is a step towards that. The WHATWG is an association of major browser developers, and is lead by representatives from Apple, Google, Mozilla, and Microsoft, who have agreed to align their browsers with the specifications developed at the WHATWG. In fact, the WebKit team have recently been rewriting their URL parser, and the latest Safari preview is now fully aligned with the URL Living Standard.

This library's parser is derived from the reference parser described in the standard, and should be fully compatible with it. It is tested using the same, common Web-Platform-Tests repository used by the major browser vendors to ensure compliance with the standard, as well as hundreds more tests covering the additional APIs.

The WebURL library is entirely portable, having no dependencies other than the Swift standard library itself. Additionally, the parser and URL operations are free from OS-dependent behaviour; one benefit of the URL Living Standard is that all parser quirks, such as handling Windows drive letters or "/" vs "\" separators, work the same way on all platforms.

We will eventually add some OS-dependent behaviour, to convert file URLs to a local file-path, but it will be clearly apart from the rest of the API. We'll leave the actual manipulation of that path to OS libraries like swift-system (e.g. I don't think it's our place to implement something like URL.resolveSymlinksInPath()).

Speed

WebURL is fast. Parsing speed depends a lot on the particular URL, but on my Intel Mac, compared to the existing URL, I've seen improvements from 10%, all the way up to 67%. On a lower-end machine (a Raspberry Pi 4), I've seen orders of magnitude improvement - benchmarks which take 1.9 seconds with the existing URL, but require only ~62 milliseconds with WebURL. Common operations like hashing and checking for equality are consistently more than twice as fast as the existing URL type.

Measuring parsing speed against the existing URL is a bit of an apples-to-oranges comparison because the standards are so different, but it's a reference point people care about. There's still more that can be done to increase WebURL's parsing speed; I have some ideas, and I'll be trying them in the coming months.

API

But perhaps the coolest thing about WebURL is its API. Here are a couple of highlights:

  1. In-place mutation

    If you want to set a property on a WebURL, you don't need to create a URLComponents and change the value and make another URL object; with WebURL, you just set the value:

    var url = WebURL("http://github.com/karwa/swift-url/")!
    
    // Upgrade to https:
    url.scheme = "https"
    url.serialized // "https://github.com/karwa/swift-url/"
    
    // Change the path:
    url.path = "/apple/swift/"
    url.serialized // "https://github.com/apple/swift/"
    

    Because WebURL is a copy-on-write value type, this replacement can happen in-place, without needing to allocate a bunch of intermediate objects.

  2. PathComponents view

    WebURL provides an efficient Collection of the URL's path components, which shares storage with the URL object:

    var url = WebURL("http://github.com/karwa/swift-url/")!
    
    for component in url.pathComponents {
      // component = "karwa", "swift-url", ""
    }
    if url.pathComponents.dropLast().last == "swift-url" {
      // ...
    }
    

    Not only that, but you can also mutate through this view. It's amazing, you have to try it:

    var url = WebURL("file:///swift-url/Sources/WebURL/WebURL.swift")!
    
    url.pathComponents.removeLast() 
    // url = "file:///swift-url/Sources/WebURL"
    
    url.pathComponents.append("My Folder")
    // url = "file:///swift-url/Sources/WebURL/My%20Folder"
    
    url.pathComponents.removeLast(3)
    
    url.pathComponents += ["Tests", "WebURLTests", "WebURLTests.swift"]
    // url = "file:///swift-url/Tests/WebURLTests/WebURLTests.swift"
    

    It also has index-based mutations functions, including a full replaceSubrange, which you can use together with the Collection implementation to perform more complex path manipulation. Again, all of this can happen in-place, so the days when URL operations were much slower than DIY string manipulation or regexes should be a thing of the past.

  3. Query parameters

    WebURL has a similar kind of view for the key-value pairs which are often encoded in a URL's query string. Here, we use @dynamicMemberLookup to create a concise and convenient syntax:

    var url = WebURL("https://example.com/currency/convert?amount=20&from=EUR&to=USD")!
    
    url.formParams.amount // "20"
    url.formParams.to // "USD
    
    url.formParams.amount = "56"
    url.formParams.to = "Pound Sterling"
    // url = "https://example.com/currency/convert?amount=56&from=EUR&to=Pound+Sterling"
    
  4. And more!

    And there's a lot more than that! WebURL comes with a lot of utility functionality necessary for working with URLs, including:

    • pure-Swift IP address types, including parsing and serialisation compatible with inet_aton/pton and inet_ntoa/ntop.
    • Percent-encoding and decoding (both eager and lazy)
    • Host objects and Origins
    • A UTF-8 view, allowing for arbitrary slices of URLs and modifying a URL's components using generic collections of UTF-8 bytes.

Roadmap

The implementation is extensively tested, but the interfaces have not had time to stabilise.

While the package is in its pre-1.0 state, it may be necessary to make source-breaking changes. I'll do my best to keep these to a minimum, and any such changes will be accompanied by clear documentation explaining how to update your code.

Aside from stabilising the API, the other priorities for v1.0 are:

  1. file URL <-> file path conversion.
  2. Converting to/from Foundation.URL.
  3. Benchmarking and optimizing setters, including modifications via pathComponents and formParams views.

For more information, see the project's README, where I go over some of the issues I'll expect to encounter along the way. I know that for a lot of macOS/iOS code, compatibility with URLSession is critical; unfortunately I don't have an excellent answer for that right now. I go over some of the issues in the README, but the short answer is that even if we could convert reliably, round-tripping would break Hashable and Equatable because there's a good chance we'll need to add percent-encoding along the way. So the answer isn't obvious, but I'm going to be trying out a variety of approaches to see what's possible.

But yeah! That's an overview of WebURL. Again, I'd encourage you to check out the Getting Started guide, and the API Reference. If you're using async-http-client, consider experimenting with the WebURL port, or if you're developing another server library in Swift, consider trying out WebURL in a branch, and letting me know how it goes for you.

I'd love to see this library adopted by as many other libraries and applications as possible, so if there's anything I can add to make that easier, please file a GitHub issue or write a post on the Swift forums. I’ll be requesting a sub-forum in the “related projects” section to discuss specific issues and ideas.

Thanks a lot for your time! I hope you're able to make use of WebURL in your projects!

55 Likes

Looks interesting.

I don't see a way to modify the path extension. Like changing myfile.jpg to myfile.zip. I do that a lot with file URLs.

2 Likes

Not yet, but that sounds like a useful thing to add. Thanks for the suggestion.

I suppose we'd want to add something like that as an extension on String, since you can have folders with extensions anywhere in the path (e.g. macOS bundles).

Paths are just so popular. If you take a look at the Foundation.URL API, you'll see that paths are disproportionally represented - probably at least half the API is dedicated to various path operations. Finding the right set of useful functionality without adding OS functionality for file paths has been a challenge.

Why not name the type URI?

2 Likes

I've been looking forward to this since this thread last year. (I actually started using version 0.0.2 a few days before 0.1.0 was announced here.) My main use case is for working with partially percent-encoded URLs, and WebURL works flawlessly.

@Karl explained in the same 1-year old thread here.

2 Likes

Fair enough!

Haha yes, I've been working on it for about a year now in my spare time. I managed to port the reference parser from the standard relatively easily, but no matter how hard I tried, performance was just way off compared with Foundation.

There are a lot of reasons to consider replacing Foundation's URL. We can't really be stuck on the 1994 standard forever, even when the rest of the world has moved on (or is in the process of doing so), and we no longer parse or behave like URLs on other systems. Swift does need a modern URL library, but I didn't want it to come with the caveat that it's 10x slower.

I've had to really dig deep and study the standard and the standard library to meet those performance goals; CFURL's parser has had literally decades of attention, and is written in C (the extra safety checks added by Swift alone create a 20% overhead - it's worth it to avoid UB, but it makes getting this sort of performance exceptionally challenging compared with a language like C). Then the scope increased quite a lot once I decided that any decent URL library would also need to add a full set of APIs for things like percent-encoding and path manipulation, and all of that needed to have very, very comprehensive tests because URLs are full of obscure edge-cases.

So it all took a little longer than expected, but I'm happy with the result. I'm glad to hear that WebURL is working for you!

I'm not completely in love with the name WebURL, but the standard does make a point of standardising on 'URL'. The 'Web' part sort of has meaning, too, since the WHATWG is a web technologies working group, and their overall mission is first and foremost to unify and standardise how web browsers behave.

9 Likes

This looks great. However I am having problems in integrating the package with SPM in my app. It fails with some resolution error. Is this a known issue?

Hi @ENorris and welcome to the forums! Glad to hear you’d like to use this package in your app :slight_smile:

Can you give any more information about the error you’re seeing, and which platform and version of Xcode/Swift you’re seeing it on?

Hi, Karl. Thanks for creating this library.

We use WebURL for decoding URLs with Cyrillic and Russian symbols. Seems that frontend, web browsers, Android and backed don't have such problems.
For example Foundation's URL can't be created from "www.mydomain.ru/search?text=банан" directly. But WebURL instance is successfully created. webUrl.serialized provides well formed percent encoded url string, which is used to create Foundation.URL.
I don't know why Foundation.URL don't make it, but once again, thank you.

Later I will share some feedback on using it when more code will be touched.

7 Likes

Hi Dmitriy,

I'm really happy to hear that WebURL is working for you! :slight_smile:

And of course, feedback is very much appreciated. Whilst we can't change the parsing/serialisation behaviour (because the standard defines them), the rest of the API can be however we want it to be. So with community involvement I'm hoping we can create the URL API that we all want to use in Swift.

When it comes to WebURL -> Foundation.URL conversion, most things should just work. I've done some brief experiments, and the main thing to watch for is that Foundation wants more things to be percent-encoded (e.g. the caret symbol, ^, in a URL's path). Technically, older standards required this, but it turns out that many browsers don't encode it, so it isn't included in the latest standard.

Foundation compatibility is clearly very important, and that will be the focus once 0.2.0 releases (soon).

The biggest thing left for 0.2.0 is to rewrite the guide. It needs to do a better job of explaining the WebURL "model" and showing what the benefits are. As you've noticed, the lenient parser and normalised output can be very convenient -- but it's actually a lot more than that. It turns out that some servers send URLs that are not strictly well-formed in HTTP redirects and Location: headers, so this behaviour is important for web compatibility. Browsers are fine with those URLs, and libraries such as cURL have a bunch of hacks to deal with them, but Swift libraries such as async-http-client don't (since they tend to use Foundation.URL for resolving redirects).

If anybody's interested in the details, this is a (draft) snippet from the guide which goes in to more depth. I've included your example, Dmitriy.

(Draft guide snippet) A Closer Look At: Parsing and Web Compatibility

A Closer Look At: Parsing and Web Compatibility

The URL Standard defines how URL strings are parsed to create an object, and how that object is later serialized to a URL string. This means that constructing a WebURL from a string doesn't only parse - it also normalizes the URL, based on how the parser interpreted it. There are some significant benefits to this; whilst parser is very lenient (literally, as lenient as a web browser), the result is a clean, simplified URL, free of many of the "quirks" required for web compatibility.

This is a very different approach to Foundation's URL, which tries hard to preserve the string exactly as you provide it (even if it ends up being needlessly strict), and offers operations such as .standardize() to clean things up later. Let's take a look at some examples which illustrate this point:

import Foundation
import WebURL

// Foundation requires your strings to be properly percent-encoded in advance.
// WebURL is more lenient, adds encoding where necessary.

URL(string: "http://example.com/some path/") // nil, fails to parse
WebURL("http://example.com/some path/")      // "http://example.com/some%20path/

// This can be a particular problem for developers if their strings might contain
// Unicode characters.

URL(string: "http://example.com/search?text=банан") // nil, fails to parse
WebURL("http://example.com/search?text=банан")      // "http://example.com/search?text=%D0%B1%D0%B0%D0%BD%D0%B0%D0%BD"

// Common syntax error: too many slashes. Browsers are quite forgiving about this,
// because HTTP URLs with empty hosts aren't even valid.
// WebURL is as lenient as a browser.

URL(string: "http:///example.com/foo") // "http:///example.com/foo", .host = nil
WebURL("http:///example.com/foo")      // "http://example.com/foo",  .host = "example.com"

// Lots of normalization:
// - IP address rewritten in canonical form,
// - Default port removed,
// - Path simplified.
// The results look nothing alike.

URL(string: "http://0x7F.1:80/some_path/dir/..") // "http://0x7F.1:80/some_path/dir/.."
WebURL("http://0x7F.1:80/some_path/dir/..")      // "http://127.0.0.1/some_path/"

One of the issues developers sometimes discover (after hours of debugging!) is that while types like Foundation's URL conform to a URL standard, there are actually multiple, incompatible URL standards(!). Whichever URL library you are using, whether Foundation's URL, cURL, or Python's urllib, may not match how your server, browser, or Java/Rust/C++/etc clients interpret URLs.

"Running different parsers and assuming that they end up with the exact same result is futile and, unfortunately, naive" says Daniel Stenberg, lead developer of the cURL library. It sounds incredible, but it's absolutely true, and it should surprise nobody that these varying and ambiguous standards lead to bugs - some of which are just annoying, but others can be catastrophic, exploitable vulnerabilities.

And multiple, incompatible standards are just part of the problem; the other part is that those standards fail to match reality on the web today, meaning browsers can't conform to them without breaking the web (!!!). The quirks shown above (e.g. being lenient about spaces and slashes) aren't limited to user input via the address bar. There are reports of servers sending URLs with spaces in HTTP redirects, having too many slashes in their HTTP Location: headers, or including non-ASCII characters. Browsers are fine with those things, but then you try the same in your App and it doesn't work.

All of this is why most URL libraries abandoned formal standards long ago - "Not even curl follows any published spec very closely these days, as we’re slowly digressing for the sake of 'web compatibility'" (Stenberg). To make things worse, each library incorporated different ad-hoc compatibility hacks, because there wasn't any standard describing what, precisely, "web compatibility" even meant.

So URLs are in a pretty sorry state. But how do we fix them? Yet another standard? Well, admittedly, yes :sweat_smile: - BUT one developed for the web, as it really is, which browsers can also conform to. No more guessing or ad-hoc compatibility hacks.

WebURL brings web-compatible URL parsing to Swift. It conforms to the new URL standard developed by major browser vendors, its author is an active participant in the standard's development, and it is validated using the shared web-platform-tests browsers use to test their own standards compliance. That means we can say with confidence that WebURL precisely matches how the new URL parser in Safari 15 behaves, and Chrome and Firefox are working to catch up. This standard is also used by JavaScript's native URL class (including NodeJS), and new libraries are being developed for many other languages which also align to the new standard.

By using WebURL in your application (especially if you use a WebURL-native request library, such as our async-http-client fork), you can guarantee that your application handles URLs just like a browser, with the same, high level of interoperability with legacy and "quirky" systems. The lenient parsing and normalization behavior shown above is a huge part of it; this is what "web compatibility" means.

6 Likes

Honestly, my colleagues looked at me like to an alien, when I asked them about problems with decoding of url string. Another problem is that sometimes URLs are not created by server, they come from external resources where they are created by content managers. And we can not ask them to fix the url and replace unicode symbols.

Sometimes, url is valid, but contains spaces in the beginning or at the end (because of human mistakes).

Another case is Deeplink URLs. Technically, we can create deeplink url, but it is tricky.

  let urlString = "ru.myapp://profile/edit"
  let url = URL(string: urlString)!
  url.scheme // "ru.myapp"
  url.host // "profile" – invalid, profile is not a host, it is part of the path
  url.path // "/edit"

It is interesting to know how backend frameworks like Vapor deal with this. Can anybody share some details on this topic?

Hi Karl, this library looks really interesting for lots of reasons! I have a specific use case that I was wondering if WebURL could fulfill, and read through the docs quite a bit but may have missed it.

Is it possible to construct a proper URL from text that resembles the style of URL you'd often see with user input? In my case I have a TextField where a user is going to type out a URLs such forums.swift.org/t/announcing-weburl-a-new-url-type-for-swift or www.github.com, often times without a proper scheme. I'd like to find a way to resolve that against the canonical representation of a URL, for example https://www.github.com to make sure I normalize and deduplicate inputs.

I completely understand if that's not a part of what you envision WebURL should do, but I figured it was worth asking if there's a way that WebURL handles that before hand-rolling my own solution.

Thanks again for the hard work and thoughtful idea!

1 Like

Hi!

Currently, WebURL just implements the parser and other algorithms defined in the URL Standard. Browsers tend to make a bunch of adjustments when consuming human-entered URLs, and those algorithms are not standardised (it's more of a competitive feature and not essential for compatibility).

I'm definitely interested in adding higher-level, more UI-focussed features like this to WebURL eventually, probably in a separate module from the core URL type but living in the same package. But even though we don't implement those things right now, I can point you to some resources which might help:

Chromium's utilities for this live in its "url formatter" component (alongside some other things I'd love to add to WebURL, like formatting and smart truncation/elision). I would be remiss not to draw attention to the readme in that folder:

Broadly, consuming human-entered URLs happens via "fixup", which tries to make "reasonable" adjustments to strings to convert them into URLs (e.g. auto-prepending schemes, but also many more, some of which may be surprising).
[...]
Because these functions are powerful, it's possible to introduce security risks with incautious use. Be sure you understand what you need and what they're doing before using them

The fixer itself is implemented in url_fixer.cc. Most of its adjustments are indeed related to the scheme; for example it assumes schemes with a "." are supposed to be hostnames (www.example.com:/), and if no scheme is found, it'll try replacing semicolons with ":", etc. When all else fails, it just defaults to http. Beyond the scheme, it also does a bunch of other adjustments, such as autocompleting TLDs using databases of domains (so mail.yahoo becomes mail.yahoo.com).

One thing that would help is if we threw detailed errors for parsing failures - so if it failed due to lack of scheme, you could make some of those repairs and try it again. I haven't added that so far because the standard can and does change (it's a living standard), so the errors would be quite tightly coupled to a specific version of the library. Still, it might be worth adding, with some warnings that it doesn't carry the same stability guarantees as the rest of the library (so if you use it, you should be prepared to update your code if it changes or use a fixed-version dependency).

1 Like

One more interesting detail. Despite Foundation.URL can not be created from string "ru.myapp://brand?search=молоко" directly, percent encoding is added if this deeplink is opened from Safari. Finally, in AppDelegate methods application(:, open:, options:) and application(_:, didFinishLaunchingWithOptions:) we have URL instance with percent encoded String "ru.myapp://brand?search=%D0%BC%D0%BE%D0%BB%D0%BE%D0%BA%D0%BE".

So, under the hood of Safari or iOS itself percent encoding is added to initial string. Or may be URL have some private initializer.

Right, Safari and Foundation don't use compatible URL models.

It turns out, all browsers treat URLs very differently. It was so bad, that when trying to codify HTML5, URLs had to be included as a descriptive paragraph rather than a reference to an actual standard. That's why the new standardisation effort is so important, as it's formalising all of these quirks that the web has come to depend on, and presenting a unified model for URLs on the web platform. It doesn't just apply to the address bar, either - URLs come from all sorts of places, and they need to be handled in a compatible way.

Safari 15 (the latest version) includes an entirely new URL parser which is written to the latest spec, so it should behave exactly as WebURL does. This extends to the JavaScript URL class (in fact, we even provide the entire JavaScript API as a .jsModel wrapper on WebURL. It has some quirks which we don't want in the Swift-native API, but if you're working on a cross Swift-JavaScript code base, it can be valuable to have exactly-matched APIs).

The difference in URL parsing between Safari (or WebKit in general) and Foundation has led to some clever exploits in the past. liveOverflow did quite a good video explaining one:

It turns out, you can get Foundation's URL parser in to some real tangles by throwing @ signs around. I've spent last week filing about 10 bugs for it. Like this one. Notice the password somehow comes after the hostname.

let urlA = URL(string: "http://@hostname:@password:@whydoesthishappen/")!
print(urlA)          // "http://@hostname:@password:@whydoesthishappen/"
print(urlA.host)     // "hostname"
print(urlA.password) // "@password"

// Bonus: one component is *both* the hostname and the password!
let urlC = URL(string: "http://:@hostname_and_password:@/")!
print(urlC)          // "http://:@hostname_and_password:@/"
print(urlC.host)     // "hostname_and_password"
print(urlC.password) // "@hostname_and_password:"

And if I paste that URL in to Safari? It goes to whydoesthishappen. So your App might make requests to an entirely different host server if you accidentally allow such a URL string to be created, and it won't even be able to tell that anything is wrong (no component returns the value whydoesthishappen -- it's like it doesn't exist).

5 Likes

Thanks for sharing. I've watched some other related videos, and it seems we need an expert group for addressing this.
The core team, I suppose, is busy with higher priority task. But it is interesting, is it possible for WebKit or ServerSide development team to join or curate this topic? @John_McCall
I suppose this will be a long term discussion.

For issues with Apple's first-party SDKs and Frameworks the right thing to do is what @Karl has already done: file reports using Feedback Assistant.

More broadly, I don't know what the SSWG can do, other than to both help incubate and graduate @Karl's excellent project and to encourage adoption of the WebURL type where it makes sense to do so.

4 Likes

This is exactly what I mean. WebURL is great to start with. Maybe they can also share details on what needs do server developers have, and highlight some points that are needed for broader using and gathering feedback from the community.

1 Like

I would expect the biggest issue is Foundation interoperability. Changing a currency type like URL would incur a lot of breakage, and some of them will have non-trivial fixes because WebURL has a very different API to Foundation.

Since those are issues where developers will need to consider what they're doing carefully, I explain those differences in detail in the guide (e.g. Foundation's components are automatically percent-decoded, but WebURL's are not). But I'm aware that long markdown documents are not the greatest way to convey information, and ultimately if people have code which uses URL and works for them, we shouldn't demand that they change their code and confront all of these issues.

I recently added support for converting from URL -> WebURL in main, meaning libraries such as async-http-client would be able to use WebURL internally (if they wish) while still supporting all of their clients who continue to use Foundation's URL. We successfully convert 83% of Foundation's URL test suite, and the ones we refuse to convert are weird things like plain file paths with no scheme, ports with no host, etc. These are just not valid in the WHATWG standard.

But because there are still so many weird edge cases (due to spec differences or bugs), I've been using fuzz-testing to make sure we refuse to convert any ambiguous input. That's where a lot of these Foundation bug reports are coming from; I would guess that CFURL has never been fuzz-tested against another URL implementation, so this process can help make both implementations better. The fuzzer has been absolutely amazing, and I cannot say enough good things about incorporating fuzz-testing in to your applications or libraries. With a good dictionary, it can be incredibly quick to find any issues that emerge during routine refactoring (with a test case), and now that it's done several hundred million iterations now with no failures, I'm very confident this conversion will not allow ambiguous URLs to slip through and become bugs.

So at least one half of Foundation - WebURL interoperability looks "done", at least for an initial, maximally-conversative implementation, and you can experiment with it now on the main branch (you need to import the WebURLFoundationExtras module. I wish we had cross-import overlays :sob:).

I'm currently working on porting the documentation to DocC (Preview - I'm really loving it!), but then it would probably be a good time to tag a release.

9 Likes