Use cases for combined documentation of multiple targets in Swift-DocC

I'm mainly hoping to host documentation for multiple libraries in the same package so this covers my use cases.

I don't know if it's covered by this change since it's related to Xcode but I'm also hoping that this would allow links to external (to the framework) symbols to be links. If this linking is powered by DocC then I would guess that it would work, otherwise I realise this is not in the scope of this conversation.

I also wanted to point to some other places this has been discussed:

https://github.com/apple/swift-docc-plugin/issues/12

which led to:

https://github.com/apple/swift-docc/issues/255

We'd love to see this for Vapor. We're currently made up of ~40 packages and top level packages link down to dependencies, both ones created by us and dependencies like NIO and AsyncHTTPClient.

This is definitely one of the things blocking us from adopting DocC, at least for anything other than simple API docs. (There are plenty of other blockers too which can be saved for other threads)

3 Likes

Agreed. In the original post I meant for the "Developers linking to documentation for SDKs or other pre-built dependencies" use case to cover linking to documentation for frameworks like Foundation or to documentation for the Swift standard library.

Yes. Although it's never mentioned in the original post I have been thinking about documentation versioning. I think this probably going to be necessary in order to support the "Connecting hosted documentation with fully qualified links" use case without breaking the "Documentation archives should not contain known broken links" goal.

The ability to browse versioned documentation is probably distinct enough that it should be spelled out as its own use case for future directions. Thank you for bringing that up.

I feel that it's an unspoken goal that developers shouldn't need to host their dependencies documentation. It's a little less clear—and something worth discussing—what DocC should do about links to dependencies that don't have hosted documentation (or where DocC can't know that the dependency has hosted documentation).

I'll take this as another reason why we should consider versioned documentation as an important use case and make sure it's part of the design discussion.

I want to try and understand what the cross-linking impacts are for this. Are there use cases that you feel wouldn't be solved by Snippets in the library documentation? Specifically, are there use cases that would be solved by linking to pages or other elements in the example app's potential documentation?

So far in this topic we're trying to uncover what use cases various people have for combined documentation so that we're not missing anything in the design discussion.

If you are asking about linking to frameworks linking to other frameworks or libraries that they depend on then that is covered by the "A target’s documentation should be able to link to that target's dependencies' documentation" goal.

If you have use cases for a framework linking to another framework without any explicit target dependency between the two frameworks I would love to know more about it so that we can consider that use case in the upcoming design discussion.

1 Like

swift-json’s docs have tooltips that reference SwiftNIO types, even though the library itself has no SwiftNIO dependency.

JSON.Rule<Location>

The swift-grammar and swift-json libraries are transparent! This means that its parsing rules are always zero-cost abstractions, even when applied to third-party collection types, like /swift-nio/NIOCore/ByteBufferView.

doccomment source

/// >   Tip: 
///     The ``/swift-grammar`` and ``/swift-json`` libraries are transparent!
///     This means that its parsing rules are always zero-cost abstractions, 
///     even when applied to third-party collection types, like 
///     ``/swift-nio/NIOCore/ByteBufferView``.

the /swift-nio/NIOCore/ByteBufferView symbol link used to work in Biome v0.2. in fact it is actually a valid URI if you copy and paste it into a browser.

but it is not linked anymore because Biome now registers packages in topological sort order, and swift-nio is no longer available before swift-json is registered.

1 Like

Others have already chimed in about the usual package author cases but I'll add more examples:

Cross-library (cross-package) links

This is the "A target’s documentation should be able to link to that target's dependencies' documentation" case you mention.

However I'd like to give a few more concrete examples as well as how we'd want to use these.

So libraries which use e.g. Logging (GitHub - apple/swift-log: A Logging API for Swift) would want to be able to link to them like:

``Logging/Logger``

However, not every single library using logging should replicate the logging docs (!), that's bad for SEO, instead they should link to "wherever the logger docs are hosted".

docc by itself can't know where to link to, as it depends where other libraries deploy/host their documentation. Most will do so on github pages or on packageindex.com (like so RevenueCat Documentation – Swift Package Index random example).

We will need to configure the baseURL for links to be formed, therefore I suggest a mapping file like this:

Side note: Preferably this would be configured in the SwiftPM plugin via a nice type-safe configuration, but we're lacking this feature still in SwiftPM, so for now let's go with an external configuration file.

We'd need to configure "when trying to link docs of Logging (which we know is from package swift-log), use this base url", so a mapping file could look something like this (details open to discussion of course):

doccHostingLocations:
  // defaultBaseURL: "..." // could be set optionally if most follow some pattern
  packages: 
    - name: swift-distributed-actors
      baseURL: "https://apple.github.io/swift-distributed-actors/$VERSION/documentation/$TARGET"
    - name: swift-log
      baseURL: "https://apple.github.io/swift-log/$VERSION/documentation/$TARGET"
    - name: swift-metrics
      baseURL: "https://apple.github.io/swift-metrics/docs/$VERSION/documentation/$TARGET"
   - name: mqtt-nio
     baseURL: "https://swiftpackageindex.com/swift-server-community/mqtt-nio/main/documentation/$TARGET"

Note the mqtt-nio example didn't support $VERSION since it only hosts main version for now... Other projects in my example do host docs for specific versions though, so that's why the baseURLs contain $VERSION. As we build docc documentation, we know what versions we depend on and create well-formed links to the exact versions MY project is depending on.

There could be fallbackURL as well, if $VERSION was not published, we could link check them and offer the fallback link but that's future work...

This allows us to strive in an ecosystem regardless where documentation ends up hosted. It could be all on github pages, or all on the package index, or on my website because I happen to own a company that also has swift APIs and want to host it on my site for whatever reason.

Prior art:

This is how it is solved in scaladoc, by e.g. the GitHub - ThoughtWorksInc/sbt-api-mappings: An Sbt plugin that fills apiMappings for common Scala libraries. sbt plugin.

Other examples:

Multiple target single-package documentation

This is the "Developers should be able to host multiple related targets in a single documentation archive." case, and I'd just like to give some examples:

Projects like NIO or swift-distributed-actors have their functionality split across many modules. For example:

are all the same package, and very often need to cross link between eachother; e.g. A ChannelPipeline documented in NIOHTTP1 needs to refer to NIOCore's EventLoopFuture https://apple.github.io/swift-nio/docs/current/NIOHTTP1/Extensions/ChannelPipeline.html#/s:7NIOCore15ChannelPipelineC8NIOHTTP1E21addHTTPClientHandlers8position21leftOverBytesStrategyAA15EventLoopFutureCyytGAC8PositionO_AD018RemoveAfterUpgradeL0OtF

Such links should work, and they are all hosted in the same place.

Prior art

In a previous life, during my Akka work, we solved this using a build plugin we developed called "unidoc" which collapsed all docs from many modules into a single docs page. Just for reference: GitHub - sbt/sbt-unidoc: sbt plugin to create a unified Scaladoc or Javadoc API document across multiple subprojects.

docc likely can do better than that here, and host them still separately, but know how to link between the targets/modules.

Re: Documentation archives should not contain known broken links

Links to documentation in other targets are only valid if both are hosted next to each other or if both are hosted together. Developers should have the ability to remove links to targets that won’t be hosted together.

This one I wanted to understand better... I agree that dead links are bad, but I would like to make sure that this is an option and not the only way. We definitely want to link across packages (see the first writeup in my post here).

I believe what this also means is that while we build documentation together, we can therefore issue warnings for bad symbol links to a dependency; once that has been built, we can assume the documentation of the dependency at the same version we built against, will be valid at the published url (that we point at with the baseURL).

"Landing page" for docs

You hint at it somewhat but it wasn't explicitly stated so I'd like to ask:

Generally when "hosting many documentation of many, related, modules" we'd want to have a landing page for such pages. In the sense of "Welcome to MyLibrary! This library does..." which is not strictly part of documentation of any specific target, but an overall documentation of the package. Which then goes on and links into specific targets.

This may be is a separate feature to talk about, but having to designate one of the targets "as the main one" in order to put the landing page in there is a bit weird and I'd love to improve upon this.

--

Hope these help and I'm really looking forward to all the great docs ecosystem we can get started building using these improvements :slight_smile:

2 Likes

Ah, and side-note we'd very much want to be able to link to swift docs as well: https://developer.apple.com/documentation/swift/actor etc. I believe this would fit the "baseURL" mappings as well.

I forgot to add this to the first example, but this could basically be solved using the same mechanism:

doccHostingLocations:
  modules:
    - name: Swift
      baseURL: "https://developer.apple.com/documentation/swift"
    - name: Distributed
      baseURL: "https://developer.apple.com/documentation/distributed"

etc.

Since those modules are "well known" those we could just automatically understand and link to developer docs, and not need them to be configured, but the mechanism would be the same as people configuring module baseURLs explicitly.

The same applies to the SDK and modules within it, like Foundation etc.

Yes, for a variety of reasons developers shouldn't host their dependencies' documentation.

AFAICT this link is from a target so a module so that the primary use case that we're thinking of. Do you know of any examples where NIO or swift-distributed-actors need to cross link to non-dependencies?

My thoughts on this is that based on some signal from the developer about which targets will be hosted together and which targets have hosted documentation at known locations (ideally version specific hosted documentation) DocC would be able to avoid or remove links that it knows would be broken when the documentation is hosted. The details are left for upcoming discussions but it's a simultaneous goal that developers should have control over this, either as direct configuration or as the ability to call DocC directly with custom arguments from a script.

Yes, the intention is that combined documentation would have some form of landing page(s) that are either synthesized or authored or a combination of both. This will probably have its own detailed design discussion since it's a fairly well isolated and developer facing component.

for many of my packages (like swift-png), this is what the README is for. could there be an option to just render the package README for a landing page?

dependency graphs are a DAG, but when writing documentation, you often want to have link cycles. one known issue with Biome is docs in NIOHTTP1 can link to symbols in NIOCore, but not the other way around, because doing so would create a dependency cycle.

so far, i haven’t come up with a great solution for this. it wouldn’t be a major change to enable it, since Biome already compiles documentation in two passes, but allowing this would really increase the “blast radius” when documentation for a single package is updated, since everything referring to it would also need to be rebuilt. if swift-foo contains even a single link to swift-bar, then swift-foo’s docs need to be rebuilt every time swift-bar is modified, and this doesn’t scale. as @ktoso mentioned, this can get really complex if swift-bar is hosted externally.

documentation versioning does not solve this problem in practice, because it is actually often necessary to overwrite the documentation (and associated git tags) for a particular version multiple times.

Do you have any examples of link cycles in documentation? I haven't found any but it's very hard to search for.

(I view link cycles and links to non-dependencies (like the swift-json example earlier) as two conceptually different cases to consider even if they may pose some of the same problems and share some of the same solutions.)

well, right now there wouldn’t be any because none of the 3 major documentation compilers support link cycles. but the README for swift-grammar starts with:

High-performance constructive parsing, in pure Swift. This module powers the swift-json library!

where swift-json is a consumer of swift-grammar.


edit: i forgot about @SDGGiesbrecht ’s compiler. i don’t know if it supports link cycles. i’d be interested to know if it does.

i don’t know if this would be a helpful distinction. the “limited” use-case you’re describing is basically just adding “documentation” dependency edges in addition to the normal edges. it would need corresponding dependency resolution logic, and some kind of counterpart to the Package.resolved format we currently have.

there is also a political problem where if package A decides to link to package B, then package B loses the right to link to package A, and the only reason it can’t is because the author of package A got there first.

I am not entirely sure what you are asking, partly due to confusion over what you meant by your link. That package does not seem relevant to your conversation. If you meant it as an example of the output of my documentation tool, the tool itself is described here.

Having read the last half dozen comments (but not the whole thread), I can describe the related bits of how my tool was designed, and hope that answers your question. (Although the tool itself has aged and I am in the slow process of shifting its guts over to DocC anyway.)

Unlike DocC, it was designed around packages not modules, and so the main page is itself a node in the graph, its “symbol” being the package declaration in the manifest. It knows about packages, libraries and targets by way of loading the manifest with SwiftPM, but then assembles the corresponding pages by scanning the manifest for matching declarations and extracting conventional documentation comments applied to them. While the documentation comments are familiar in format, as far as I know no other tool recognizes them, because they are technically attached to function calls, not declarations in the AST sense. The scan is also imperfect, as exotic formatting in the manifest can result in them being not found and hence being flagged as undocumented.

The tool expressly does not pick up the read‐me, but rather the other way around. It generates the read‐me along with the documentation. The reason for this is that a read‐me tends to contain a lot of information on one page that is better split up into separate pages when you have a whole website to work with. Whereas the documentation site has separate pages for the main description, importing instructions, and stuff about the author, the read‐me has them in sequential sections. The read‐me also starts with a list of supported platforms, and a link to the full documentation, which would be redundant on the documentation site, since the platform list is already in the navigation frame of every page, and the documentation link would just be circular.

Also unlike DocC, the tool does not build anything, but rather scans the AST with SwiftSyntax. This was a deliberate design decision in order to have accurate information about #if. It also means the tool has no knowledge of mangled names and no unique identifiers for nodes. The tool attempts to syntax‐highlight any code spans, including turning any known symbols into links. But due to having compiled nothing, this is imperfect and does not always do the right thing when name clashes occur. While the tool does scan dependencies (it knows about the standard and core libraries, and anything it learns from SwiftPM), it only does so to detect inheritance. It does not automatically link to anything outside the package, so calls to dependency functions appear the same as calls to hypothetical placeholders.

Manual links with fully qualified URLs can point anywhere, and that is what you have to do if you want to link to something in another package. Knowing both that users will do this, and that developers will refactor code, the tool attempts to guarantee the semi‐persistence of all page URLs. The directory structure of the site (and hence the path segment of a URL) is arranged logically. If the tool is aware of existing documentation (such as if the output directory already exists, or it is aimed at a gh-pages branch), then it starts by overwriting every file with a redirect to its parent. Then the new documentation is generated overtop, obliterating redirects wherever symbols still exist and leaving the rest behind. That way if an instance method is documented and linked to, but then removed, the otherwise dead link is now pointing at a redirect to the type page, whose list of methods most likely contains the removed method’s successor.

For the related projects section, a list of package URLs is supplied to the tool configuration. The configuration supports importing external packages, so an organization is expected to curate a list of URLs in a metadata package, and then have all of its real packages’ configurations import that metadata from the central location. The tool follows the URLs and extracts some data, but this happens outside of any dependency graph, and does not go looking for transitive related packages. While it almost always involves link cycles, it deals only with a flat list, not a deep graph. And nothing learned from related projects is fed back into to the source scanner or renderer, so the main documentation pages have no knowledge of their symbols.

1 Like

I have started to think it would be useful for SwiftPM to have a means of declaring known clients for the sake of testing. It would serve a purpose similar to Swift’s source compatibility suite, and the listed clients would be treated almost as though they were the actual roots of separate graphs and the current package were instead an overridden local reference. Such a feature might be equally useful for your upward facing documentation links.

Right that was within the same package examples.

Cross to non-dependencies I think we'd only ever do "hey, look at this other library if you're interested" but would not need specific symbol references. So those can be plain old href links I suppose.

Swift metrics and logging have examples of this:

API is designed to establish a standard that can be implemented by various metrics libraries which then post the metrics data to backends like Prometheus, Graphite, publish over statsd, write to disk, etc.

but as you can see those are just links to the other packages; we don't need to deep link into non-dependencies ever I think.

Sounds good to me!

Perhaps, I'd only be worried about making the readme not-readable if not docc rendered (people navigating to github, stash etc, the readme should look nice there too). David said that'll be a separate proposal so let's hash it out then though :slight_smile:

1 Like

This problem can probably be solved in two stages with a relatively straightforward solution. Mind you, I’m not well versed in how the internals of DoC works, so how complex this would be to implement is not considered here.

First, Xcode already uses DocC to compile documentation for everything within a project or package in Xcode locally - regardless of whether it has adopted DocC fully or has remote documentation available. Therefore, using the existing Symbol/Reference/Format, it could be updated to support the target name as the first parameter - using the Package.swift file as a reference for SPM dependencies, the project file for target references, and a statically defined list for system frameworks.

  • Foundation/String
  • swift-nio/NIOCore/Channel
  • WidgetExtension/MyWidget

Second, would be to figure out how these references translate to links in remote documentation. System frameworks would be easy, as Apple knows the URLs these are located at and can infer the version based on minimum supported system version of the package or project.

Targets and other local packages could dump their docs into a known folder format that can be referenced by relative href links. Whatever mechanism Xcode uses to determine all targets and local packages within a project/package could be ported to DocC (I assume there is something that is just invoking the docc command for each target after some scan of the project and package file to spit out local documentation).

External packages would need to define where documentation lives. This would probably make the most sense to live inside the Package.swift file as a new parameter at the top level. When documentation is generated for that package it follows the same folder structure mentioned before, so it’s not necessary for the importing package to know anything other than a base URL.

Versioning makes things a bit more complicated, but what makes the most sense would be a requirement that the documentation must be hosted in a way that mirrors the package’s Git tree - meaning that DocC would need to run by a CI step that generates documentation for every commit pushed to the package repository, then dump that in some known folder structure - maybe just commit sha/tags at a top level. The importing package can determine links by looking at its Package.resolved file, to know exactly which version of the docs to point to - whether that’s a version tag or commit SHA.

Alternatively, and what probably makes more sense, is documentation could be generated locally somehow and pushed up as part of the commits to the package itself. This would prevent the documentation from ballooning in size as a separate copy for each commit wouldn’t be needed, it would just diff alongside the rest of the source changes. This would require some implementation of local build step to generate the documentation in a way that developers don’t need to think about doing this, it just happens on a build and the /docs folder changes are there when the developer goes to commit their source changes.

Either of these could be cumbersome for the package author depending on how this is implemented, but it’s the only way to know for sure that a symbol reference won’t be broken if pointing to some arbitrary commit of a package.

These solutions make the most sense to me, if they’re possible to implement. It should be tightly coupled to the existing Package manifest system, Package resolution system, the package Git repository, etc. It should be straightforward for package authors to supply documentation in a way that requires minimal additional information to be provided (ie: no documentation manifest files, no hard coded links, etc).

versioning does not guarantee that docs will be compatible. in order of increasing likelihood:

the last two items are way more common than many people realize.

this is correct, and has already proven to be a major problem for the swiftinit.org docs. doing a back-of-the envelope calculation for swift-syntax, which has a ~60MB symbolgraph and about 500 release tags, would require approx. 30GB in storage! just for one package!

diffing symbolgraphs would first require the generated JSON to be stable, which it currently is not.

diffing symbolgraphs is also incompatible with text compression, which is currently our most effective means of compacting symbolgraph data. it would also require the JSON to be pretty-printed (right now it is emitted in minified form) for line-by-line diffing to be effective, which would further balloon the file size.

Biome has an application-specific stable symbolgraph format which is VCS-friendly and yields an approx. 8x improvement in size when uncompressed, and 2–3x compressed. but this is not sufficient (yet), because we would need an improvement by a factor of maybe 100x for long-term storage of versioned documentation to be feasable.

Thanks for all the replies so far. They've both highlighted a few new use cases and brought attention to some important details for other use cases which is exactly the outcome that I'd hoped for.

It makes sense as a next step to continue with a more technical discussion about how to build this. I have more thoughts about this and am writing up a longer post with more information. I'm leaning towards posting that as a new topic to separate the technical discussion from the use case discussion. If you have more use cases for combined documentation of multiple targets, please continue adding them to this tread/topic.

8 Likes

Hi @ronnqvist. Hope everything is going well. Was there any update on this topic?

After gathering use cases the discussion continued in this thread to talk about high-level design and implementation. After those high-level discussions I've had some conversations with people offline about more in-depth implementation details and prototyped a few future directions to explore areas where design decisions today could impact the ability to build on top of this. With the learnings from those discussions and prototypes we're shifting focus to implementing the initial feature set.

I'll let people know when it's in a state where they can try it out with their own projects.

5 Likes