├── .github └── FUNDING.yml ├── .gitignore ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── swift-dependency-graph-lib │ ├── PackageLoader.swift │ ├── Packages.swift │ ├── TaskQueue.swift │ └── http.swift └── swift-dependency-graph │ └── main.swift ├── Tests ├── LinuxMain.swift └── swift-dependency-graphTests │ ├── packages.json │ └── swift_dependency_graphTests.swift ├── html ├── chart.css ├── chart.js ├── dependencies.json ├── filter.css ├── filter.js ├── images │ ├── dependency-graph.png │ ├── pagelink.svg │ ├── solid-arrow-circle-down.svg │ └── solid-arrow-circle-up.svg └── index.html ├── packages.json ├── upload.sh └── webserver.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: adam-fowler 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "78db67e5bf4a8543075787f228e8920097319281", 10 | "version": "1.18.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-argument-parser", 15 | "repositoryURL": "https://github.com/apple/swift-argument-parser.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "9564d61b08a5335ae0a36f789a7d71493eacadfc", 19 | "version": "0.3.2" 20 | } 21 | }, 22 | { 23 | "package": "swift-atomics", 24 | "repositoryURL": "https://github.com/apple/swift-atomics.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "6c89474e62719ddcc1e9614989fff2f68208fe10", 28 | "version": "1.1.0" 29 | } 30 | }, 31 | { 32 | "package": "swift-collections", 33 | "repositoryURL": "https://github.com/apple/swift-collections.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "937e904258d22af6e447a0b72c0bc67583ef64a2", 37 | "version": "1.0.4" 38 | } 39 | }, 40 | { 41 | "package": "swift-driver", 42 | "repositoryURL": "https://github.com/apple/swift-driver.git", 43 | "state": { 44 | "branch": "release/5.4", 45 | "revision": "93e8b927225a62b963ebe13ab11e04192fa8a67b", 46 | "version": null 47 | } 48 | }, 49 | { 50 | "package": "llbuild", 51 | "repositoryURL": "https://github.com/apple/swift-llbuild.git", 52 | "state": { 53 | "branch": "release/5.4", 54 | "revision": "eb56a00ed9dfd62c2ce4ec86183ff0bc0afda997", 55 | "version": null 56 | } 57 | }, 58 | { 59 | "package": "swift-log", 60 | "repositoryURL": "https://github.com/apple/swift-log.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "32e8d724467f8fe623624570367e3d50c5638e46", 64 | "version": "1.5.2" 65 | } 66 | }, 67 | { 68 | "package": "swift-nio", 69 | "repositoryURL": "https://github.com/apple/swift-nio.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "6213ba7a06febe8fef60563a4a7d26a4085783cf", 73 | "version": "2.54.0" 74 | } 75 | }, 76 | { 77 | "package": "swift-nio-extras", 78 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "0e0d0aab665ff1a0659ce75ac003081f2b1c8997", 82 | "version": "1.19.0" 83 | } 84 | }, 85 | { 86 | "package": "swift-nio-http2", 87 | "repositoryURL": "https://github.com/apple/swift-nio-http2.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "a8ccf13fa62775277a5d56844878c828bbb3be1a", 91 | "version": "1.27.0" 92 | } 93 | }, 94 | { 95 | "package": "swift-nio-ssl", 96 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 97 | "state": { 98 | "branch": null, 99 | "revision": "e866a626e105042a6a72a870c88b4c531ba05f83", 100 | "version": "2.24.0" 101 | } 102 | }, 103 | { 104 | "package": "swift-nio-transport-services", 105 | "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", 106 | "state": { 107 | "branch": null, 108 | "revision": "41f4098903878418537020075a4d8a6e20a0b182", 109 | "version": "1.17.0" 110 | } 111 | }, 112 | { 113 | "package": "SwiftPM", 114 | "repositoryURL": "https://github.com/apple/swift-package-manager.git", 115 | "state": { 116 | "branch": "swift-5.4-RELEASE", 117 | "revision": "7cd58d6cc1945b14db1346792b39af609ce17fe9", 118 | "version": null 119 | } 120 | }, 121 | { 122 | "package": "swift-tools-support-core", 123 | "repositoryURL": "https://github.com/apple/swift-tools-support-core.git", 124 | "state": { 125 | "branch": "release/5.4", 126 | "revision": "d7bd4375c26e7dab2c17791cfa06f9b981d02339", 127 | "version": null 128 | } 129 | }, 130 | { 131 | "package": "Yams", 132 | "repositoryURL": "https://github.com/jpsim/Yams.git", 133 | "state": { 134 | "branch": null, 135 | "revision": "9003d51672e516cc59297b7e96bff1dfdedcb4ea", 136 | "version": "4.0.4" 137 | } 138 | } 139 | ] 140 | }, 141 | "version": 1 142 | } 143 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftDependencyGraph", 8 | platforms: [.macOS(.v10_13)], 9 | products: [ 10 | .executable(name: "swift-dependency-graph", targets: ["swift-dependency-graph"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.4.1"), 14 | .package(name: "SwiftPM", url: "https://github.com/apple/swift-package-manager.git", .branch("swift-5.4-RELEASE")), 15 | .package(url: "https://github.com/apple/swift-argument-parser.git", from: "0.3.0") 16 | ], 17 | targets: [ 18 | .target(name: "swift-dependency-graph", dependencies: [ 19 | "swift-dependency-graph-lib", 20 | .product(name: "ArgumentParser", package: "swift-argument-parser") 21 | ]), 22 | .target(name: "swift-dependency-graph-lib", dependencies: [ 23 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 24 | .product(name: "SwiftPM-auto", package: "SwiftPM") 25 | ]), 26 | .testTarget(name: "swift-dependency-graphTests", 27 | dependencies: ["swift-dependency-graph-lib"], 28 | resources: [.process("packages.json")]), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift dependency graph 2 | 3 | http://swift-dependency-graph.opticalaberration.com 4 | 5 | Display the dependencies or the packages dependent on a swift package in a organisational graph as seen below. 6 | 7 | ![swift-dependency-graph example](html/images/dependency-graph.png) 8 | 9 | The swift dependency graph uses the Swift Package catalog from the SwiftPMLibrary setup by Dave Verwer. 10 | -------------------------------------------------------------------------------- /Sources/swift-dependency-graph-lib/PackageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PackageLoader.swift 3 | // AsyncHTTPClient 4 | // 5 | // Created by Adam Fowler on 18/08/2019. 6 | // 7 | 8 | import Foundation 9 | import NIO 10 | import Workspace 11 | import PackageLoading 12 | import PackageModel 13 | 14 | enum PackageLoaderError : Error { 15 | case invalidUrl 16 | case invalidToolsVersion 17 | case invalidManifest 18 | case looping 19 | case gitVersionLoadingFailed(errorOutput: String) 20 | case noVersions 21 | } 22 | 23 | 24 | class PackageLoader { 25 | 26 | let threadPool: NIOThreadPool 27 | let manifestLoader: PackageManifestLoader 28 | let eventLoopGroup: EventLoopGroup 29 | let httpLoader: HTTPLoader 30 | let onAdd: (String, Package)->() 31 | let onError: (String, Error)->() 32 | let taskQueue: TaskQueue<[String]> 33 | 34 | init(onAdd: @escaping (String, Package)->(), onError: @escaping (String, Error)->() = {_,_ in }) throws { 35 | self.threadPool = NIOThreadPool(numberOfThreads: System.coreCount) 36 | self.threadPool.start() 37 | self.manifestLoader = try PackageManifestLoader() 38 | self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) 39 | self.httpLoader = HTTPLoader(eventLoopGroup: eventLoopGroup) 40 | self.onAdd = onAdd 41 | self.onError = onError 42 | self.taskQueue = .init(maxConcurrentTasks: 8, on: eventLoopGroup.next()) 43 | } 44 | 45 | func syncShutdown() throws { 46 | try httpLoader.syncShutdown() 47 | try eventLoopGroup.syncShutdownGracefully() 48 | } 49 | 50 | /// load package names from json 51 | func load(url: String, packages: Packages) throws -> [String] { 52 | if url.hasPrefix("http") { 53 | // get package names 54 | return try httpLoader.getBody(url: url) 55 | .flatMapThrowing { (buffer) throws -> [String] in 56 | let data = Data(buffer) 57 | var names = try JSONSerialization.jsonObject(with: data, options: []) as? [String] ?? [] 58 | // append self to the list 59 | names.append("https://github.com/adam-fowler/swift-dependency-graph.git") 60 | return names 61 | }.wait() 62 | } else { 63 | let data = try Data(contentsOf: URL(fileURLWithPath: url)) 64 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String] ?? [] 65 | } 66 | } 67 | 68 | /// load packages from array of package names 69 | func loadPackages(_ packages: [String]) -> Future { 70 | let futures = packages.map { name in return addPackage(url: name) 71 | .map { return () } 72 | .flatMapError { (error)->Future in 73 | self.onError(name, error) 74 | return self.eventLoopGroup.next().makeSucceededFuture(Void()) 75 | } 76 | } 77 | return EventLoopFuture.whenAllComplete(futures, on: eventLoopGroup.next()).map {_ in return () } 78 | } 79 | 80 | /// add a package, works out default branch and calls add package with branch name, then calls onAdd callback 81 | func addPackage(url: String) -> Future { 82 | // get package.swift from default branch 83 | return self.getDefaultBranch(url: url).flatMap { (branch)->Future<[String]> in 84 | return self.taskQueue.submitTask { self.addPackage(url: url, version: branch) } 85 | } 86 | .map { buffer in 87 | print("Adding \(url)") 88 | self.onAdd(url, Package(dependencies: buffer)) 89 | } 90 | } 91 | 92 | func addPackage(url: String, version: String?) -> Future<[String]>{ 93 | 94 | let repositoryUrl : String 95 | if let url = PackageLoader.getRawRepositoryUrl(url: url, version: version) { 96 | repositoryUrl = url 97 | } else { 98 | return self.eventLoopGroup.next().makeFailedFuture(PackageLoaderError.invalidUrl) 99 | } 100 | // Order of loading is 101 | // - Package@swift-5.swift 102 | // - Package.swift 103 | // - Package@swift-4.2.swift 104 | // - Package@swift-4.swift 105 | var errorPassedDown : Error? = nil 106 | var packageUrlToLoad = repositoryUrl + "/Package@swift-5.swift" 107 | return self.httpLoader.getBody(url: packageUrlToLoad) 108 | 109 | .flatMapError { (error)->Future<[UInt8]> in 110 | packageUrlToLoad = repositoryUrl + "/Package.swift" 111 | return self.httpLoader.getBody(url: packageUrlToLoad) 112 | } 113 | .flatMap { (buffer)->Future<[String]> in 114 | return self.manifestLoader.load(buffer, url: packageUrlToLoad, on: self.eventLoopGroup.next()) 115 | } 116 | .flatMapError { (error)->Future<[String]> in 117 | errorPassedDown = error 118 | packageUrlToLoad = repositoryUrl + "/Package@swift-4.2.swift" 119 | return self.httpLoader.getBody(url: packageUrlToLoad) 120 | .flatMap { buffer in 121 | return self.manifestLoader.load(buffer, url: packageUrlToLoad, on: self.eventLoopGroup.next()) 122 | } 123 | } 124 | .flatMapError { (error)->Future<[String]> in 125 | packageUrlToLoad = repositoryUrl + "/Package@swift-4.swift" 126 | return self.httpLoader.getBody(url: packageUrlToLoad) 127 | .flatMap { buffer in 128 | return self.manifestLoader.load(buffer, url: packageUrlToLoad, on: self.eventLoopGroup.next()) 129 | } 130 | } 131 | .flatMapErrorThrowing { error in 132 | throw errorPassedDown ?? error 133 | } 134 | 135 | } 136 | 137 | func getDefaultBranch(url: String) -> Future { 138 | return threadPool.runIfActive(eventLoop: eventLoopGroup.next()) { ()->String in 139 | guard let lsRemoteOutput = try? Process.checkNonZeroExit( 140 | args: Git.tool, "ls-remote", "--symref", url, "HEAD", environment: Git.environment).spm_chomp() else {return "master"} 141 | // split into tokens separated by space. The second token is the branch ref. 142 | let branchRefTokens = lsRemoteOutput.components(separatedBy: CharacterSet.whitespacesAndNewlines) 143 | var branch : Substring? = nil 144 | if branchRefTokens.count > 1, branchRefTokens[1].hasPrefix("refs/heads/") { 145 | // split branch ref by '/'. Last element is branch name 146 | branch = branchRefTokens[1].dropFirst(11) 147 | } 148 | if let branch = branch { 149 | return String(branch) 150 | } 151 | return "master" 152 | } 153 | } 154 | 155 | func getLatestVersion(url: String) -> Future { 156 | let regularExpressionXXX = try! NSRegularExpression(pattern: "[0-9]+\\.[0-9]+\\.[0-9]+$", options: []) 157 | let regularExpressionXX = try! NSRegularExpression(pattern: "[0-9]+\\.[0-9]+$", options: []) 158 | // Look into getting versions 159 | // git ls-remote --tags . 160 | return threadPool.runIfActive(eventLoop: eventLoopGroup.next()) { ()->String? in 161 | guard let lsRemoteOutput = try? Process.checkNonZeroExit( 162 | args: Git.tool, "ls-remote", "--tags", url, environment: Git.environment).spm_chomp() else {return nil} 163 | let tags = lsRemoteOutput.split(separator: "\n").compactMap { $0.split(separator:"/").last } 164 | let versions = tags 165 | .map {String($0)} 166 | .compactMap { (versionString)->(v:Version, s:String)? in 167 | /// if of form major.minor.patch 168 | let cleanVersionString : String 169 | let firstMatchRangeXXX = regularExpressionXXX.rangeOfFirstMatch(in: versionString, options: [], range: NSMakeRange(0, versionString.count)) 170 | if let range = Range(firstMatchRangeXXX, in: versionString) { 171 | cleanVersionString = String(versionString[range]) 172 | } else { 173 | /// if of form major.minor 174 | let firstMatchRangeXX = regularExpressionXX.rangeOfFirstMatch(in: versionString, options: [], range: NSMakeRange(0, versionString.count)) 175 | if let range = Range(firstMatchRangeXX, in: versionString) { 176 | cleanVersionString = String(versionString[range])+".0" 177 | } else { 178 | return nil 179 | } 180 | } 181 | if let version = Version(string: cleanVersionString) { 182 | return (version, versionString) 183 | } 184 | return nil 185 | } 186 | .sorted {$0.v < $1.v} 187 | .map {$0.s} 188 | 189 | return versions.last 190 | } 191 | } 192 | 193 | /// get URL from github repository name 194 | static func getRawRepositoryUrl(url: String, version: String? = nil) -> String? { 195 | let url = Packages.cleanupName(url) 196 | 197 | // get Package.swift URL 198 | var split = url.split(separator: "/", omittingEmptySubsequences: false) 199 | if split.last == "" { 200 | split = split.dropLast() 201 | } 202 | 203 | if split.count > 2 && split[2] == "github.com" { 204 | split[2] = "raw.githubusercontent.com" 205 | } else if split.count > 2 && split[2] == "gitlab.com" { 206 | split.append("raw") 207 | } 208 | 209 | if let version = version { 210 | split.append("\(version)") 211 | } else { 212 | split.append("master") 213 | } 214 | 215 | return split.joined(separator: "/") 216 | } 217 | 218 | /// return if this is a valid repository name 219 | static func isValidUrl(url: String) -> Bool { 220 | let split = url.split(separator: "/", omittingEmptySubsequences: false) 221 | if split[0].hasPrefix("git@github.com") && split.count == 2 222 | || split.count > 4 && split[2] == "github.com" 223 | || split.count > 4 && split[2] == "www.github.com" 224 | || split.count > 4 && split[2] == "gitlab.com" 225 | || split.count > 4 && split[2] == "www.gitlab.com" { 226 | return true 227 | } 228 | return false 229 | } 230 | } 231 | 232 | public class PackageManifestLoader { 233 | 234 | let resources: UserManifestResources 235 | let loader: ManifestLoader 236 | 237 | public init() throws { 238 | self.resources = try UserManifestResources(swiftCompiler: swiftCompiler, swiftCompilerFlags: []) 239 | self.loader = ManifestLoader(manifestResources: resources) 240 | } 241 | 242 | // We will need to know where the Swift compiler is. 243 | var swiftCompiler: AbsolutePath = { 244 | let string: String 245 | #if os(macOS) 246 | string = try! Process.checkNonZeroExit(args: "xcrun", "--sdk", "macosx", "-f", "swiftc").spm_chomp() 247 | #else 248 | string = try! Process.checkNonZeroExit(args: "which", "swiftc").spm_chomp() 249 | #endif 250 | return AbsolutePath(string) 251 | }() 252 | 253 | public func load(_ buffer: [UInt8], url: String, on eventLoop: EventLoop) -> EventLoopFuture<[String]> { 254 | let promise = eventLoop.makePromise(of: [String].self) 255 | let fs = InMemoryFileSystem() 256 | 257 | print("Loading manifest from \(url)") 258 | 259 | do { 260 | try fs.createDirectory(AbsolutePath("/Package")) 261 | try fs.writeFileContents(AbsolutePath("/Package/Package.swift"), bytes: ByteString(buffer)) 262 | 263 | var toolsVersion = try ToolsVersionLoader().load(at: AbsolutePath("/Package/"), fileSystem: fs) 264 | if toolsVersion < ToolsVersion.minimumRequired { 265 | print("error: Package version is below minimum, trying minimum") 266 | toolsVersion = .minimumRequired 267 | } 268 | loader.load( 269 | package: AbsolutePath("/Package/"), 270 | baseURL: AbsolutePath("/Package/").pathString, 271 | toolsVersion: toolsVersion, 272 | packageKind: .local, 273 | fileSystem: fs, 274 | on: DispatchQueue.global() 275 | ) { result in 276 | switch result { 277 | case .failure(let error): 278 | switch error { 279 | case PackageLoaderError.invalidManifest: 280 | promise.fail(error) 281 | case is ManifestParseError: 282 | promise.fail(PackageLoaderError.invalidManifest) 283 | default: 284 | print("Error loading \(url) \(error)") 285 | promise.fail(error) 286 | } 287 | case .success(let manifest): 288 | let dependencies = manifest.dependencies.map {$0.url} 289 | promise.succeed(dependencies) 290 | } 291 | } 292 | } catch { 293 | print("Error loading \(url) \(error)") 294 | promise.fail(error) 295 | } 296 | return promise.futureResult 297 | } 298 | } 299 | 300 | 301 | -------------------------------------------------------------------------------- /Sources/swift-dependency-graph-lib/Packages.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Packages.swift 3 | // AsyncHTTPClient 4 | // 5 | // Created by Adam Fowler on 18/08/2019. 6 | // 7 | 8 | import Foundation 9 | import NIO 10 | 11 | typealias Future = EventLoopFuture 12 | 13 | public struct Package : Codable { 14 | /// has the package been setup fully with its dependencies setup 15 | public var readPackageSwift: Bool = false 16 | /// packages we are dependent on 17 | var dependencies: Set 18 | /// packages dependent on us 19 | var dependents: Set 20 | /// error 21 | var error : String? { 22 | // if setting an error the package must have been read 23 | didSet { readPackageSwift = true} 24 | } 25 | 26 | public init() { 27 | self.dependencies = [] 28 | self.dependents = [] 29 | } 30 | 31 | public init(dependencies: [String]) { 32 | self.readPackageSwift = true 33 | self.dependencies = Set(dependencies.map { Packages.cleanupName($0) }) 34 | self.dependents = [] 35 | } 36 | 37 | public func encode(to encoder: Encoder) throws { 38 | var container = encoder.container(keyedBy: CodingKeys.self) 39 | // enocde dependencies and dependents in alphabetical order 40 | try container.encode(dependencies.map{$0}.sorted(by:{ return $0.split(separator: "/").last! < $1.split(separator: "/").last! }), forKey: .dependencies) 41 | try container.encode(dependents.map{$0}.sorted(by:{ return $0.split(separator: "/").last! < $1.split(separator: "/").last! }), forKey: .dependents) 42 | try container.encode(error, forKey: .error) 43 | } 44 | 45 | enum CodingKeys : String, CodingKey { 46 | case dependencies = "on" 47 | case dependents = "to" 48 | case error = "error" 49 | } 50 | } 51 | 52 | enum PackagesError : Swift.Error { 53 | case corruptDependencies 54 | } 55 | 56 | public class Packages { 57 | public typealias Container = [String: Package] 58 | public private(set) var packages : Container 59 | 60 | public init() throws { 61 | self.packages = [:] 62 | self.loader = try PackageLoader(onAdd: self.add, onError: self.addLoadingError) 63 | } 64 | 65 | public init(packages: Container) throws { 66 | self.packages = packages 67 | self.loader = try PackageLoader(onAdd: self.add, onError: self.addLoadingError) 68 | 69 | // flag all packages as read 70 | for key in self.packages.keys { 71 | self.packages[key]?.readPackageSwift = true 72 | } 73 | } 74 | 75 | /// add a package 76 | func add(name: String, package: Package) { 77 | let name = Packages.cleanupName(name) 78 | lock.lock() 79 | defer { 80 | lock.unlock() 81 | } 82 | 83 | var package = package 84 | // if package already exists then add the dependent of the original package to the new one 85 | if let package2 = packages[name] { 86 | guard package2.readPackageSwift != true else {return} 87 | package.dependents = package2.dependents 88 | } 89 | packages[name] = package 90 | 91 | for dependency in package.dependencies { 92 | // guard against invalid urls. If invalid remove from dependency list 93 | guard PackageLoader.isValidUrl(url: dependency) else { 94 | print("Error: removed dependency as the URL(\(dependency) was invalid") 95 | packages[name]?.dependencies.remove(dependency) 96 | continue 97 | } 98 | let dependencyName = Packages.cleanupName(dependency) 99 | if packages[dependencyName] == nil { 100 | packages[dependencyName] = Package() 101 | } 102 | packages[dependencyName]!.dependents.insert(name) 103 | } 104 | } 105 | 106 | /// set loading package failed 107 | public func addLoadingError(name: String, error: Error) { 108 | print("Failed to load package from \(name) error: \(Packages.stringFromError(error))") 109 | let name = Packages.cleanupName(name) 110 | lock.lock() 111 | defer { 112 | lock.unlock() 113 | } 114 | 115 | // if package already exists 116 | let error = Packages.stringFromError(error) 117 | if packages[name] != nil { 118 | packages[name]?.error = error 119 | } else { 120 | var package = Package() 121 | package.error = error 122 | packages[name] = package 123 | } 124 | } 125 | 126 | /// import packages.json file 127 | /// - Parameters: 128 | /// - url: URL of packages json file 129 | /// - iterations: Number of iterations we will run emptying the package array after having added dependencies 130 | public func `import`(url: String, iterations : Int = 100) throws { 131 | // Load package names from url 132 | let packageNames = try loader.load(url: url, packages: self).map { Packages.cleanupName($0)} 133 | 134 | try loadPackages(packageNames, iterations: iterations) 135 | 136 | } 137 | 138 | /// Load list of packages 139 | /// - Parameters: 140 | /// - packageNames: List of package URLs 141 | /// - iterations: Number of iterations we will run emptying the package array after having added dependencies 142 | func loadPackages(_ packageNames: [String], iterations : Int = 100) throws { 143 | // remove duplicate packages, sort and remove packages we have already loaded 144 | var packageNames = Array(Set(packageNames)).sorted().compactMap { (name)->String? in 145 | let name = Packages.cleanupName(name) 146 | return packages[name] == nil ? name : nil 147 | } 148 | 149 | var iterations = iterations 150 | repeat { 151 | try loader.loadPackages(packageNames).wait() 152 | 153 | // verify we havent got stuck in a loop 154 | iterations -= 1 155 | guard iterations > 0 else { throw PackageLoaderError.looping } 156 | 157 | // create new list of packages containing packages that haven't been loaded 158 | packageNames = packages.compactMap {return !$0.value.readPackageSwift ? $0.key : nil} 159 | } while(packageNames.count > 0) 160 | } 161 | 162 | /// Remove package from dependency set, also needs to remove all of its dependecies 163 | /// - Parameter packageName: URL of package 164 | public func removePackage(_ packageName: String) throws { 165 | let name = Packages.cleanupName(packageName) 166 | guard let package = packages[name] else { return } 167 | // when you remove a package you have to remove it from its dependencies dependents lists 168 | for d in package.dependencies { 169 | guard var dependency = packages[d] else { throw PackagesError.corruptDependencies } 170 | dependency.dependents.remove(name) 171 | } 172 | // when you remove a package you have to remove it dependents 173 | for d in package.dependents { 174 | try removePackage(d) 175 | } 176 | packages[name] = nil 177 | } 178 | 179 | /// Remove packages filtered by including a string 180 | /// - Parameter filteredBy: String that packages need to contain to be removed 181 | public func removePackages(filteredBy: String) throws { 182 | let packages = self.packages.compactMap { (entry)->String? in 183 | if entry.key.contains(filteredBy) { 184 | return entry.key 185 | } 186 | return nil 187 | } 188 | try packages.forEach { try self.removePackage($0); print("Rebulding \($0)") } 189 | } 190 | 191 | /// save dependency file 192 | public func save(filename: String) throws { 193 | let encoder = JSONEncoder() 194 | encoder.outputFormatting = .sortedKeys 195 | let data = try encoder.encode(packages) 196 | try data.write(to: URL(fileURLWithPath: filename)) 197 | } 198 | 199 | /// convert error to string 200 | static func stringFromError(_ error: Swift.Error) -> String { 201 | switch error { 202 | case PackageLoaderError.invalidToolsVersion: 203 | return "Requires later version of Swift" 204 | case PackageLoaderError.invalidManifest: 205 | return "InvalidManifest" 206 | case HTTPLoader.HTTPError.failedToLoad(_): 207 | return "FailedToLoad" 208 | default: 209 | return "\(error)" 210 | } 211 | } 212 | 213 | /// convert name from github/repository.git to github/repository 214 | public static func cleanupName(_ packageName: String) -> String { 215 | var packageName = packageName.lowercased() 216 | // if package is recorded as git@github.com changes to https://github.com/ 217 | if packageName.hasPrefix("git@github.com") { 218 | var split = packageName.split(separator: "/", omittingEmptySubsequences: false) 219 | let split2 = split[0].split(separator: ":") 220 | //guard split2.count > 1 else {return nil} 221 | // set user name 222 | split[0] = split2[1] 223 | split.insert("github.com", at:0) 224 | split.insert("", at:0) 225 | split.insert("https:", at:0) 226 | packageName = split.joined(separator: "/") 227 | } 228 | 229 | if packageName.suffix(4) == ".git" { 230 | // remove .git suffix 231 | return String(packageName.prefix(packageName.count - 4)) 232 | } else if packageName.last == "/" { 233 | // ensure name doesn't end with "/" 234 | return String(packageName.dropLast()) 235 | } 236 | return packageName 237 | } 238 | 239 | let lock = NSLock() 240 | var loader: PackageLoader! 241 | 242 | 243 | } 244 | 245 | -------------------------------------------------------------------------------- /Sources/swift-dependency-graph-lib/TaskQueue.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Soto for AWS open source project 4 | // 5 | // Copyright (c) 2017-2020 the Soto project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of Soto project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIO 16 | 17 | /// Manage a queue of tasks, ensuring only so many tasks are running concurrently. Based off code posted by 18 | /// Cory Benfield on Vapor Discord. https://discord.com/channels/431917998102675485/448584561845338139/766320821206908959 19 | class TaskQueue { 20 | struct PendingTask { 21 | let task: () -> EventLoopFuture 22 | let promise: EventLoopPromise 23 | 24 | init(_ task: @escaping () -> EventLoopFuture, on eventLoop: EventLoop) { 25 | self.task = task 26 | self.promise = eventLoop.makePromise(of: Value.self) 27 | } 28 | } 29 | 30 | let maxConcurrentTasks: Int 31 | var currentTasks: Int 32 | let eventLoop: EventLoop 33 | var queue: CircularBuffer> 34 | 35 | init(maxConcurrentTasks: Int, on eventLoop: EventLoop) { 36 | self.maxConcurrentTasks = maxConcurrentTasks 37 | self.eventLoop = eventLoop 38 | self.queue = CircularBuffer(initialCapacity: maxConcurrentTasks) 39 | self.currentTasks = 0 40 | } 41 | 42 | func submitTask(_ task: @escaping () -> EventLoopFuture) -> EventLoopFuture { 43 | self.eventLoop.flatSubmit { 44 | let task = PendingTask(task, on: self.eventLoop) 45 | 46 | if self.currentTasks < self.maxConcurrentTasks { 47 | self.invoke(task) 48 | } else { 49 | self.queue.append(task) 50 | } 51 | 52 | return task.promise.futureResult 53 | } 54 | } 55 | 56 | private func invoke(_ task: PendingTask) { 57 | self.eventLoop.preconditionInEventLoop() 58 | precondition(self.currentTasks < self.maxConcurrentTasks) 59 | 60 | self.currentTasks += 1 61 | task.task().hop(to: self.eventLoop).whenComplete { result in 62 | self.currentTasks -= 1 63 | self.invokeIfNeeded() 64 | task.promise.completeWith(result) 65 | } 66 | } 67 | 68 | private func invokeIfNeeded() { 69 | self.eventLoop.preconditionInEventLoop() 70 | 71 | if let first = self.queue.popFirst() { 72 | self.invoke(first) 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Sources/swift-dependency-graph-lib/http.swift: -------------------------------------------------------------------------------- 1 | // 2 | // http.swift 3 | // AsyncHTTPClient 4 | // 5 | // Created by Adam Fowler on 18/08/2019. 6 | // 7 | 8 | import Foundation 9 | import NIO 10 | import AsyncHTTPClient 11 | 12 | 13 | public class HTTPLoader { 14 | enum HTTPError : Error { 15 | case noPackageBody(String) 16 | case failedToLoad(String) 17 | case moved(String?) 18 | } 19 | 20 | let eventLoopGroup : EventLoopGroup 21 | let client : HTTPClient 22 | 23 | public init(eventLoopGroup : EventLoopGroup) { 24 | self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 25 | self.client = HTTPClient(eventLoopGroupProvider: .shared(self.eventLoopGroup)) 26 | } 27 | 28 | func syncShutdown() throws { 29 | try client.syncShutdown() 30 | } 31 | 32 | public func get(url: String) -> EventLoopFuture { 33 | return client.get(url: url, deadline: .now() + .seconds(30)).flatMapThrowing { (response)->HTTPClient.Response in 34 | guard response.status != .movedPermanently else {throw HTTPError.moved(response.headers["Location"].first)} 35 | guard response.status != .found else {throw HTTPError.moved(response.headers["Location"].first)} 36 | guard response.status == .ok else {throw HTTPError.failedToLoad(url)} 37 | return response 38 | } 39 | .flatMapError { (error)->EventLoopFuture in 40 | switch error { 41 | case HTTPError.moved(let newUrl): 42 | if let url = newUrl { 43 | return self.get(url: url) 44 | } 45 | default: 46 | break 47 | } 48 | return self.eventLoopGroup.next().makeFailedFuture(error) 49 | } 50 | } 51 | 52 | public func getBody(url: String) -> EventLoopFuture<[UInt8]> { 53 | return get(url: url).flatMapThrowing { (response)->[UInt8] in 54 | guard let body = response.body else {throw HTTPError.noPackageBody(url)} 55 | guard let bytes = body.getBytes(at: 0, length: body.readableBytes) else {throw HTTPError.noPackageBody(url)} 56 | return bytes 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/swift-dependency-graph/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import swift_dependency_graph_lib 4 | import IOKit.pwr_mgt 5 | import ArgumentParser 6 | 7 | class System { 8 | /// prevent system going to sleep. Returns an id you have to supply to re-enable system sleep 9 | class func preventSleep(reason:String) -> IOPMAssertionID? { 10 | var assertionID = IOPMAssertionID(0) 11 | let success = IOPMAssertionCreateWithName(kIOPMAssertPreventUserIdleSystemSleep as CFString, 12 | IOPMAssertionLevel(kIOPMAssertionLevelOn), 13 | reason as CFString, 14 | &assertionID) 15 | if success == kIOReturnSuccess { 16 | return assertionID 17 | } 18 | return nil 19 | } 20 | 21 | /// re-enable system sleep. Pass in the id returned from the matching preventSleep() call 22 | class func allowSleep(id : IOPMAssertionID) { 23 | IOPMAssertionRelease(id) 24 | } 25 | } 26 | 27 | let rootPath = #file.split(separator: "/", omittingEmptySubsequences: false).dropLast(3).joined(separator: "/") 28 | 29 | struct SwiftDependencyGraph: ParsableCommand { 30 | // output path 31 | @Option var output: String = rootPath + "/html/dependencies.json" 32 | 33 | // rebuild all flag 34 | @Flag(help: "Rebuild all packages") var rebuildAll: Bool = false 35 | 36 | // rebuild package option 37 | @Option(name: .shortAndLong, help: "Rebuild package and its dependents") var rebuild: String? 38 | 39 | func run() throws { 40 | let startTime = Date() 41 | let id = System.preventSleep(reason: "Swift Dependency Graph") 42 | defer { 43 | if let id = id { 44 | System.allowSleep(id: id) 45 | } 46 | } 47 | 48 | let url = "https://raw.githubusercontent.com/SwiftPackageIndex/PackageList/main/packages.json" 49 | 50 | do { 51 | let packages: Packages 52 | // load json that is already there 53 | if !rebuildAll { 54 | let data = try Data(contentsOf: URL(fileURLWithPath: self.output)) 55 | let packageList = try JSONDecoder().decode(Packages.Container.self, from: data) 56 | packages = try Packages(packages: packageList) 57 | if let rebuild = rebuild { 58 | try packages.removePackages(filteredBy: rebuild) 59 | } 60 | } else { 61 | packages = try Packages() 62 | } 63 | try packages.import(url: url) 64 | try packages.save(filename: output) 65 | } catch { 66 | print(error) 67 | } 68 | print("Dependency generation took \(Int(-startTime.timeIntervalSinceNow)) seconds") 69 | } 70 | } 71 | 72 | SwiftDependencyGraph.main() 73 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import swift_dependency_graphTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += swift_dependency_graphTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/swift-dependency-graphTests/packages.json: -------------------------------------------------------------------------------- 1 | [ 2 | "https://github.com/adam-fowler/swift-dependency-graph.git", 3 | "https://github.com/AndyQ/NFCPassportReader.git", 4 | "https://github.com/carson-katri/swift-request.git", 5 | "https://github.com/cmtrounce/SwURL.git", 6 | "https://github.com/dmytro-anokhin/url-image.git", 7 | "https://github.com/egeniq/BetterSheet.git", 8 | "https://github.com/enablex/VCXSocket.git", 9 | ] 10 | -------------------------------------------------------------------------------- /Tests/swift-dependency-graphTests/swift_dependency_graphTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import swift_dependency_graph_lib 3 | 4 | func attempt(function: () throws -> ()) { 5 | do { 6 | try function() 7 | } catch { 8 | XCTFail(error.localizedDescription) 9 | } 10 | } 11 | 12 | final class swift_dependency_graphTests: XCTestCase { 13 | 14 | var packageLoader: PackageLoader! 15 | 16 | override func setUp() { 17 | XCTAssertNil(self.packageLoader) 18 | XCTAssertNoThrow(self.packageLoader = try PackageLoader(onAdd: { _,_ in })) 19 | } 20 | 21 | override func tearDown() { 22 | XCTAssertNoThrow(try self.packageLoader.syncShutdown()) 23 | self.packageLoader = nil 24 | } 25 | 26 | func testPackagesCleanupName() { 27 | XCTAssertEqual(Packages.cleanupName("https://github.com/user/repository"), "https://github.com/user/repository") 28 | XCTAssertEqual(Packages.cleanupName("https://github.com/user/repository/"), "https://github.com/user/repository") 29 | XCTAssertEqual(Packages.cleanupName("https://github.com/user/repository.git"), "https://github.com/user/repository") 30 | } 31 | 32 | func testLoadPackageTest() { 33 | attempt { 34 | try packageLoader.addPackage(url: "https://github.com/adam-fowler/swift-dependency-graph").wait() 35 | } 36 | } 37 | 38 | func testLoadRedirectPackageTest() { 39 | attempt { 40 | try packageLoader.addPackage(url: "http://github.com/adam-fowler/swift-dependency-graph").wait() 41 | } 42 | } 43 | 44 | func testLoadGitlabWithUppercaseLetterPackageTest() { 45 | attempt { 46 | try packageLoader.addPackage(url: "https://gitlab.com/Mordil/swift-redi-stack").wait() 47 | } 48 | } 49 | 50 | func testLoadGitAtGitHubPackageTest() { 51 | attempt { 52 | try packageLoader.addPackage(url: "git@github.com:adam-fowler/swift-dependency-graph").wait() 53 | } 54 | } 55 | 56 | func testLoadPackageV4Test() { 57 | attempt { 58 | try packageLoader.addPackage(url: "https://github.com/getguaka/env.git").wait() 59 | } 60 | } 61 | 62 | func testLoadPackageV4_2Test() { 63 | attempt { 64 | try packageLoader.addPackage(url: "https://github.com/apple/swift-protobuf").wait() 65 | } 66 | } 67 | 68 | // invalid manifest (trying to load Package.swift when it is for swift 3) 69 | func testLoadPackageSwiftV4Test() { 70 | attempt { 71 | try packageLoader.addPackage(url: "https://github.com/jdhealy/prettycolors").wait() 72 | try packageLoader.addPackage(url: "https://github.com/vapor/json.git").wait() 73 | } 74 | } 75 | 76 | func testLoadPackageV5_1Test() { 77 | attempt { 78 | try packageLoader.addPackage(url: "https://github.com/Jimmy-Lee/Networking.git").wait() 79 | } 80 | } 81 | 82 | func testOnReleaseNotMaster() { 83 | attempt { 84 | // Package.swift on master branch is corrupt but release branch version is fine 85 | try packageLoader.addPackage(url: "https://github.com/httpswift/swifter.git").wait() 86 | // no master branch but there is a release with package available 87 | try packageLoader.addPackage(url: "https://github.com/tomlokhorst/xcodeedit").wait() 88 | } 89 | } 90 | 91 | func testReleasesInvalidVersionTags() { 92 | attempt { 93 | // Release numbers have a v suffix 94 | try packageLoader.addPackage(url: "https://github.com/Bilue/ContentFittingWebView").wait() 95 | } 96 | } 97 | 98 | func testReleasesNumbersNotFormattedCorrectly() { 99 | attempt { 100 | // Release numbers are incorrect using major.minor not major.minor.patch 101 | try packageLoader.addPackage(url: "https://github.com/Alecrim/AlecrimAsyncKit").wait() 102 | try packageLoader.addPackage(url: "https://github.com/alexdrone/Store").wait() 103 | } 104 | } 105 | 106 | func testReleaseNumbersThatGoHigherThanNine() { 107 | attempt { 108 | // Releases are sorted alphabetically. Need to create version object for release and sort those 109 | try packageLoader.addPackage(url: "https://github.com/Carthage/Commandant").wait() 110 | } 111 | } 112 | 113 | func testOnMasterButNotRelease() { 114 | attempt { 115 | // Package.swift doesn't exist in the branch, while there is one on master 116 | try packageLoader.addPackage(url: "https://github.com/abdullahselek/TakeASelfie").wait() 117 | } 118 | } 119 | 120 | func testLoadNonMasterPackageTest() { 121 | attempt { 122 | // Package.swift not on master and there are no releases 123 | try packageLoader.addPackage(url: "https://github.com/Flinesoft/AnyMenu.git").wait() 124 | } 125 | } 126 | 127 | func testLoadingDependencyWithWWWPrefix() throws { 128 | let packages = try Packages() 129 | try packages.loadPackages(["https://github.com/krad/memento.git"]) 130 | XCTAssertNotNil(packages.packages["https://www.github.com/krad/clibavcodec"]) 131 | } 132 | 133 | func testLoadingDuplicates() throws { 134 | let packages = try Packages() 135 | try packages.loadPackages(["https://github.com/adam-fowler/swift-dependency-graph"]) 136 | try packages.loadPackages(["https://github.com/adam-fowler/swift-dependency-graph"]) 137 | } 138 | 139 | func testLoadPackageWithSlashInDefaultBranch() { 140 | attempt { 141 | try packageLoader.addPackage(url: "https://github.com/openkitten/bson").wait() 142 | } 143 | } 144 | 145 | func testLoadErroringPackage() { 146 | attempt { 147 | } 148 | } 149 | /* 150 | 151 | */ 152 | /* 153 | Failed to load package from https://github.com/mattt/surge error: FailedToLoad (no Package.swift on default branch) 154 | Failed to load package from https://github.com/enablex/VCXSocket.git error: FailedToLoad Doesn't exist 155 | Failed to load package from https://github.com/mdaxter/bignumgmp.git error: InvalidManifest empty Package.swift 156 | Failed to load package from https://github.com/vzsg/ed25519.git error: InvalidManifest empty Package.swift 157 | Failed to load package from https://github.com/kthomas/jwtdecode.swift.git error: FailedToLoad doesn't exist 158 | Failed to load package from https://github.com/dentelezhkin/dwifft.git error: InvalidManifest empty Package.swift 159 | Failed to load package from https://github.com/kthomas/uickeychainstore.git error: FailedToLoad doesn't exist 160 | */ 161 | 162 | 163 | func testLoadPackageJson() { 164 | attempt { 165 | let rootFolder = #file 166 | .split(separator:"/", omittingEmptySubsequences: false) 167 | .dropLast(1) 168 | .map { String(describing: $0) } 169 | .joined(separator:"/") 170 | let packages = try Packages() 171 | try packages.import(url: rootFolder + "/packages.json", iterations: 8) 172 | 173 | XCTAssertNotNil(packages.packages["https://github.com/adam-fowler/swift-dependency-graph"]) 174 | XCTAssertNotNil(packages.packages["https://github.com/apple/swift-package-manager"]) 175 | XCTAssertNotNil(packages.packages["https://github.com/apple/swift-llbuild"]) 176 | XCTAssertNotNil(packages.packages["https://github.com/enablex/vcxsocket"]?.error) 177 | } 178 | } 179 | 180 | static var allTests = [ 181 | ("testPackagesCleanupName", testPackagesCleanupName), 182 | ] 183 | } 184 | -------------------------------------------------------------------------------- /html/chart.css: -------------------------------------------------------------------------------- 1 | .google-visualization-orgchart-table { 2 | padding: 16px; 3 | } 4 | .chart-node { 5 | text-align: left; 6 | vertical-align: middle; 7 | border: 1px solid #b5d9ea; 8 | padding: 0; 9 | -webkit-border-radius: 3px; 10 | -webkit-box-shadow: rgba(0, 0, 0, 0.5) 3px 3px 3px; 11 | background-color: #edf7ff; 12 | background: -webkit-gradient(linear, left top, left bottom, from(#edf7ff), to(#cde7ee)); 13 | } 14 | .packagetitle { 15 | font-weight: 400; 16 | min-width: 60px; 17 | } 18 | .packageowner { 19 | font-weight: 300; 20 | font-size: 70%; 21 | } 22 | .packagelink { 23 | /*position: relative; 24 | display: block; 25 | width: 16px; 26 | height: 16px; 27 | top: 2px; 28 | right: 2px;*/ 29 | } 30 | .packagelink img { 31 | vertical-align: middle; 32 | } 33 | /* override google-visualization-orgchart-table styles for links and images */ 34 | .google-visualization-orgchart-table a { 35 | padding: 0px; 36 | } 37 | .google-visualization-orgchart-table img { 38 | padding: 0px; 39 | } 40 | .expand { 41 | margin-left: auto; 42 | margin-right: auto; 43 | text-align: center; 44 | } 45 | #details { 46 | position: absolute; 47 | bottom: 20px; 48 | left: 20px; 49 | width: 300px; 50 | max-width: 85%; 51 | background-color: #ccc; 52 | padding: 12px; 53 | font-size: 80%; 54 | } 55 | #minimize-button { 56 | position: absolute; 57 | display: block; 58 | top: 5px; 59 | right: 5px; 60 | padding: 1px 2px 1px 2px; /* Add some padding */ 61 | border: 1px solid #000; 62 | font-size: 60%; 63 | } 64 | #chart_div { 65 | width: 100%; 66 | } 67 | 68 | @media screen and (max-width: 480px) { 69 | #details { 70 | bottom: 12px; 71 | left: 12px; 72 | padding: 8px; 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /html/chart.js: -------------------------------------------------------------------------------- 1 | /* 2 | Chart building code 3 | */ 4 | 5 | var chart 6 | var chart_data 7 | var direction = "on" 8 | var expandedNode 9 | var dependencyData 10 | var dependencyFile = "https://raw.githubusercontent.com/adam-fowler/swift-dependency-graph/main/html/dependencies.json?v=9" 11 | var rootName = "https://github.com/adam-fowler/swift-dependency-graph" 12 | var nodeId = 0 13 | var nodePositions = {} 14 | var nodeToView 15 | 16 | // load google chart 17 | google.charts.load('current', {packages:["orgchart"]}); 18 | google.charts.setOnLoadCallback(loadDependencies); 19 | 20 | parseQueryParams() 21 | 22 | function parseQueryParams() { 23 | var queryDict = {} 24 | location.search.substr(1).split("&").forEach(function(item) {queryDict[item.split("=")[0]] = item.split("=")[1]}) 25 | 26 | if (queryDict["package"] != undefined) { 27 | // root name without git extension. regex removes extension 28 | rootName = queryDict["package"].replace(/\.[^\//.]+$/, "") 29 | } 30 | if (queryDict["dependents"] != undefined && queryDict["dependents"] != 0) { 31 | direction = "to" 32 | } 33 | } 34 | 35 | function createChart() { 36 | // Create the chart. 37 | let container = document.getElementById('chart_div') 38 | chart = new google.visualization.OrgChart(container); 39 | 40 | container.addEventListener('click', function (e) { 41 | e.preventDefault(); 42 | if (e.target.tagName.toUpperCase() === 'A') { 43 | chart.setSelection([]); 44 | gtag('event', 'view_link', {'event_label' : e.target.href}) 45 | window.open(e.target.href, "_blank"); 46 | drawChart() 47 | } else if (e.target.parentElement.tagName.toUpperCase() === 'A') { 48 | chart.setSelection([]); 49 | gtag('event', 'view_link', {'event_label' : e.target.parentElement.href}) 50 | window.open(e.target.parentElement.href, "_blank"); 51 | drawChart() 52 | } else { 53 | let selection = chart.getSelection() 54 | if (selection.length == 0) { 55 | return 56 | } 57 | let value = chart_data.getValue(selection[0].row, 0) 58 | let newRoot = value.split('#')[0] 59 | if (newRoot == "__next__") { 60 | let rootName = value.split('#')[1] 61 | var position = 1 62 | if (nodePositions[rootName] != undefined) { 63 | position = nodePositions[rootName] + 1 64 | } 65 | nodePositions[rootName] = position 66 | } 67 | else if (newRoot == "__prev__") { 68 | let rootName = value.split('#')[1] 69 | var position = 0 70 | if (nodePositions[rootName] != undefined) { 71 | position = nodePositions[rootName] - 1 72 | } 73 | nodePositions[rootName] = position 74 | } 75 | else if (newRoot == "__expand__") { 76 | expandedNode = value 77 | //nodeToView = 78 | } 79 | else if (newRoot == rootName) { 80 | if (direction == "on") { direction = "to" } else { direction = "on" } 81 | } else { 82 | rootName = newRoot 83 | } 84 | chart.setSelection([]); 85 | drawChart(); 86 | } 87 | }, false); 88 | 89 | } 90 | 91 | function loadDependencies() { 92 | $.ajax({ 93 | url : dependencyFile, 94 | success : function (data) { 95 | dependencyData = JSON.parse(data) 96 | let keys = Object.keys(dependencyData) 97 | let filterNames = keys.map(function(name) {return displayName(name)}) 98 | setFilterListOptions(filterNames, keys, "selectRoot") 99 | createChart(); 100 | drawChart() 101 | } 102 | }); 103 | } 104 | 105 | function selectRoot(name) { 106 | rootName = name 107 | gtag('event', 'view_package', {'event_label' : rootName}) 108 | direction = "on" 109 | hideFilterList() 110 | drawChart() 111 | } 112 | 113 | function displayName(name) { 114 | let split = name.split("/") 115 | if (split.length >= 2) { 116 | return `${split[split.length-2]}/${split[split.length-1]}` 117 | } 118 | return name 119 | } 120 | 121 | function idName(id) { 122 | return id.replace(/\W/g, '') 123 | } 124 | function renderName(name, id) { 125 | let split = name.split("/") 126 | if (split.length >= 2) { 127 | var html = [] 128 | let id2 = idName(id) 129 | html.push(`
${split[split.length-1]}
`) 130 | html.push(`
${split[split.length-2]}`) 131 | html.push(`
`) 132 | return html.join("") 133 | } 134 | return name 135 | } 136 | 137 | function tooltipForName(name) { 138 | let node = dependencyData[name] 139 | if (node.error === "FailedToLoad") { 140 | return `${name}\nDependencies unavailable. Cannot find a Package.swift on the 'master' branch` 141 | } else if(node.error === "InvalidManifest") { 142 | return `${name}\nDependencies unavailable. Failed to load Package.swift.\nEither it is corrupt or is an unsupported version.\nVersions supported range from 4.0 to 5.0.` 143 | } else if(node.error === "Unknown") { 144 | return `${name}\nDependencies unavailable. Failed to load Package.swift` 145 | } 146 | return name 147 | } 148 | 149 | function addChildrenRows(data, rootName, level, rootNameCount, packagesAdded, stack = []) { 150 | nodeId += 1 151 | if (level == 0) { 152 | return 153 | } 154 | let maxPackages = 12 155 | let nodeId2 = nodeId 156 | let root = dependencyData[rootName] 157 | let numChildren = root[direction].length 158 | var position = 0 159 | 160 | if (nodePositions[rootName] != undefined) { 161 | position = nodePositions[rootName] 162 | } 163 | var packagesToDisplay = root[direction] 164 | // if viewing dependents limit number to 12 165 | if (direction == "to") { 166 | packagesToDisplay = packagesToDisplay.slice(position*maxPackages,(position+1)*maxPackages) 167 | } 168 | var rows = packagesToDisplay.map(function(name){return [{v:`${name}#${nodeId2}`, f:renderName(name, `${name}#${nodeId2}`)}, rootNameCount, tooltipForName(name)]}) 169 | 170 | stack.push(rootName) 171 | // if we have already display a complete tree for a package and it has more than 4 children show expand box 172 | if (packagesAdded.has(rootName) && numChildren > 4) { 173 | let stackString = stack.join("#") 174 | let name = `__expand__#${stackString}` 175 | if (name != expandedNode) { 176 | let row = data.addRow([{v:name, f:`

\u2193

`}, rootNameCount, "Show more ..."]) 177 | //data.setRowProperty(row, "parent", ) 178 | stack.pop() 179 | return 180 | } 181 | } 182 | 183 | // if node position is greater than zero than add previous node 184 | if (position > 0 && direction == "to") { 185 | data.addRow([{v:`__prev__#${rootName}`, f:"

\u2190

"}, rootNameCount, "Show more ..."]) 186 | } 187 | data.addRows(rows) 188 | // if there are more than maxPackages to display then add a next button 189 | if (root[direction].length - position*maxPackages > maxPackages && direction == "to") { 190 | data.addRow([{v:`__next__#${rootName}`, f:"

\u2192

"}, rootNameCount, "Show more ..."]) 191 | } 192 | 193 | 194 | packagesAdded.add(rootName) 195 | 196 | for(entry in packagesToDisplay) { 197 | let rootName = packagesToDisplay[entry] 198 | addChildrenRows(data, rootName, level-1, `${rootName}#${nodeId2}`, packagesAdded, stack) 199 | } 200 | stack.pop() 201 | } 202 | 203 | function drawChart() { 204 | var packagesAdded = new Set([]) 205 | 206 | chart_data = new google.visualization.DataTable(); 207 | 208 | chart_data.addColumn('string', 'Name'); 209 | chart_data.addColumn('string', 'Parent'); 210 | chart_data.addColumn('string', 'ToolTip'); 211 | 212 | let root = dependencyData[rootName] 213 | nodeId = 0 214 | chart_data.addRows([[{v:rootName, f:renderName(rootName, rootName)}, "", tooltipForName(rootName)]]); 215 | addChildrenRows(chart_data, rootName, 8, rootName, packagesAdded) 216 | console.log(`Number of nodes ${nodeId}`) 217 | // set background 218 | var backgroundImages = {"to" : "images/solid-arrow-circle-down.svg", "on" : "images/solid-arrow-circle-up.svg"} 219 | document.body.style.backgroundImage = `url(${backgroundImages[direction]})` 220 | 221 | // Draw the chart, setting the allowHtml option to true for the tooltips. 222 | chart.draw(chart_data, {allowHtml:true, nodeClass:"chart-node", selectedNodeClass:"chart-node"}); 223 | 224 | //var position = $('#httpsgithubcomvaporfluent73').offset(); 225 | //console.log(position) 226 | } 227 | -------------------------------------------------------------------------------- /html/filter.css: -------------------------------------------------------------------------------- 1 | #filter { 2 | width: 66%; 3 | max-width: 500px; 4 | display: inline-block; 5 | position: relative; 6 | } 7 | 8 | #filterInput { 9 | /*background-image: url('/css/searchicon.png'); Add a search icon to input */ 10 | background-position: 10px 12px; /* Position the search icon */ 11 | background-repeat: no-repeat; /* Do not repeat the icon image */ 12 | width: 100%; 13 | box-sizing: border-box; 14 | font-size: 100%; /* Increase font-size */ 15 | padding: 12px 20px 12px 20px; /* Add some padding */ 16 | border: 1px solid #ddd; /* Add a grey border */ 17 | margin-bottom: 12px; /* Add some space below the input */ 18 | } 19 | 20 | #filterList { 21 | /* Remove default list styling */ 22 | list-style-type: none; 23 | min-width: 100%; /* Full-width */ 24 | padding: 0; 25 | margin: 0; 26 | display: block; 27 | position: absolute; 28 | z-index: 1; 29 | } 30 | 31 | #filterList li { 32 | border: 1px solid #ddd; /* Add a border to all links */ 33 | margin-top: -1px; /* Prevent double borders */ 34 | background-color: #f6f6f6; /* Grey background color */ 35 | padding: 12px 20px 12px 20px; /* Add some padding */ 36 | text-decoration: none; /* Remove default text underline */ 37 | font-size: 100%; /* Increase the font-size */ 38 | color: black; /* Add a black text color */ 39 | display: block; /* Make it into a block element to fill the whole list */ 40 | } 41 | 42 | #filterList li:hover:not(.header) { 43 | background-color: #eee; /* Add a hover effect to all links, except for headers */ 44 | } 45 | 46 | @media screen and (max-width: 480px) { 47 | #filterInput { 48 | padding: 6px 10px 6px 10px; /* Add some padding */ 49 | margin-bottom: 6px; /* Add some space below the input */ 50 | } 51 | #filterList li { 52 | padding: 6px 10px 6px 10px; /* Add some padding */ 53 | font-size: 90%; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /html/filter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JS Filter list 3 | */ 4 | 5 | var filterNames = [] 6 | var filterData = [] 7 | var onClickFunctionName = "" 8 | 9 | function setFilterListOptions(strings, data, onClick) { 10 | filterNames = strings 11 | filterData = data 12 | onClickFunctionName = onClick 13 | } 14 | 15 | function filterList() { 16 | // Declare variables 17 | var input, filter, ul, i, txtValue; 18 | input = document.getElementById('filterInput'); 19 | filter = input.value.toUpperCase(); 20 | ul = document.getElementById("filterList"); 21 | 22 | if (filter.length <= 2) { 23 | filter = "@%@%@" 24 | } 25 | let html = [] 26 | // Loop through all list items, and hide those who don't match the search query 27 | for (i = 0; i < filterNames.length; i++) { 28 | txtValue = filterNames[i] 29 | if (txtValue.toUpperCase().indexOf(filter) > -1) { 30 | html.push(`
  • ${filterNames[i]}
  • `) 31 | } 32 | } 33 | ul.innerHTML = html.join("") 34 | } 35 | 36 | function hideFilterList() { 37 | var ul = document.getElementById("filterList"); 38 | ul.innerHTML = "" 39 | } 40 | -------------------------------------------------------------------------------- /html/images/dependency-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adam-fowler/swift-dependency-graph/4d10c83d9f34f79249674367b21254f947b6c4df/html/images/dependency-graph.png -------------------------------------------------------------------------------- /html/images/pagelink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /html/images/solid-arrow-circle-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /html/images/solid-arrow-circle-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 42 | 43 | 44 |
    45 | 46 |
      47 |
      48 |
      49 |

      Swift Dependency Graph

      50 |

      Start to type the name of a package in the search bar and select from the menu to display a package and its dependencies. Click on a node in the graph to make that the top node. Click on the top node to toggle between displaying a package's dependencies and displaying the packages dependent on it.

      51 |

      The arrow in the background indicates whether you are displaying dependents or dependencies. The arrow always points in the direction of the parent or dependent package.

      52 |

      The swift dependency graph uses the Swift Package catalog from the Swift Package Index setup by Dave Verwer. The graph is written by Adam Fowler.

      53 |
      54 |
      55 | 56 | 57 | -------------------------------------------------------------------------------- /packages.json: -------------------------------------------------------------------------------- 1 | [ 2 | "https://gitlab.com/Mordil/swift-redi-stack", 3 | "git@github.com:kandelvijaya/AlgorithmChecker.git", 4 | "https://github.com/amzn/smoke-aws-credentials.git", 5 | "https://github.com/amzn/smoke-framework.git", 6 | "https://github.com/apple/swift-llbuild.git", 7 | "https://github.com/DuetHealth/DrX.git", 8 | "https://github.com/swift-aws/aws-sdk-swift.git", 9 | "https://github.com/adam-fowler/swift-dependency-graph.git" 10 | ] 11 | -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export AWS_PROFILE=website-admin 4 | 5 | aws s3 sync html/ s3://swift-dependency-graph.com/ 6 | -------------------------------------------------------------------------------- /webserver.sh: -------------------------------------------------------------------------------- 1 | cd html/ 2 | python -m SimpleHTTPServer 8001 3 | --------------------------------------------------------------------------------