Three folders, three rules
Open Voyager's repository and the first file worth reading is not a view or a model. It is Package.swift, and its header comment is the whole architecture in four lines: a parent, an engine child, and a Mac shell. One package called Voyager builds three targets and exports three products: two libraries, VoyagerCore and VoyagerWebKit, and the executable Voyager.
Each target is a folder under Sources/, and each folder holds one kind of knowledge.
| Target | Role | Depends on | Swift files |
|---|---|---|---|
VoyagerCore | The parent: tabs, sessions, identities, trust, the departure log, the family catalog, the Pippa host | CwkKitCore | 26 |
VoyagerWebKit | The engine child: the system's WebKit behind the root contract | VoyagerCore | 6 |
VoyagerApp | The Mac shell: window, tab strip, address field, menus | both, plus CwkKitCore, CwkKitMac, CwkKitMedia | 26 |
The arrows point one way
Read the dependency lists as arrows. The child depends on the parent. The shell depends on both. Nothing points back at the child, and SwiftPM refuses a cycle, so the parent cannot even name WebKitEngine: that type lives in a module VoyagerCore never imports. This half of the rule is enforced by the compiler.
The other half is not. WebKit, AppKit and UIKit are system frameworks, and any target on an Apple platform can import them without asking SwiftPM. The manifest's comment promises "No WebKit, AppKit or UIKit import" for the parent, and on the current main branch the promise holds: the parent's 26 files import Foundation, Observation, CwkKitCore and a few system libraries (Security, SQLite3, CommonCrypto), and not one of them contains an #if os branch. No test checks it, though. It holds because architecture invariant 2 says "No parent code imports WebKit or branches on the platform", and every change is read against that line.
Why the libraries are products
An executable target alone would have been enough for a Mac app. The two libraries are products for a reader that did not exist yet. The very first commit with code in it (a73d23c, 2026-10-03) already says the package is "consumed by the Mac app here and by the iPhone and iPad app". The phone target was started 23 days later (22dc017, 2026-10-26). When it came, its XcodeGen project pointed at the same package by a relative path and linked the two products. Nothing was copied and nothing was ported. The phone app became a second shell on the same parent and the same child, which is the subject of lesson 5.
Two test targets, two kinds of truth
VoyagerCoreTests depends on VoyagerCore and the kit's CwkKitTesting, and on nothing that can draw a page. It holds 242 tests in 48 suites, and each one checks a rule of the parent against a fake engine or against no engine at all. VoyagerWebKitTests depends on the parent and the child, and drives real WKWebViews inside the test process: 63 tests in 8 suites. One xcrun swift test runs both. Lessons 3 and 4 open each of them.
The split is the point. A failure in the first target means a rule of the browser is broken. A failure in the second means WebKit does something the child did not expect. You never have to guess which of the two you are looking at, because the target that failed already says.
What else the manifest carries
- The platform floor.
.macOS("15.4"), .iOS("18.4"), the first releases withWKWebExtension. The first brick said.macOS(.v15), .iOS(.v18); the commit that brought the extension host (1360d8d) raised it. Because the manifest names the floor, no@availablecheck rides the code. - Swift 6 language mode for every target, so strict concurrency checking is on everywhere. Swift Quest covers what that buys and what it costs.
- The family's shared Swift, by path.
.package(path: "../cwkSiblingKit/CwkKit"), the way every other family app consumes it. Sibling Kit Quest explains why the family shares code like this and never forks it.