├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── build-and-deploy-docs-workflow.yml │ └── deploy-api-docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── api-docs.png ├── error.html ├── generate-api-docs.swift ├── generate-package-api-docs.swift ├── images ├── article.svg ├── collection.svg ├── curly-brackets.svg └── technology.svg ├── index.html ├── stack.yaml └── theme-settings.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @0xTim @gwynne 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | groups: 8 | dependencies: 9 | patterns: 10 | - "*" 11 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy-docs-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Shared Build DocC docs and Deploy 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.ref }} 4 | cancel-in-progress: true 5 | on: 6 | workflow_call: 7 | inputs: 8 | package_name: 9 | type: string 10 | required: false 11 | description: "The name of the package to build docs for." 12 | modules: 13 | type: string 14 | required: true 15 | description: "The modules in the package to build docs for." 16 | pathsToInvalidate: 17 | type: string 18 | required: false 19 | description: "The paths to invalidate in CloudFront, e.g. '/vapor/* /xctvapor/*'." 20 | workflow_dispatch: 21 | inputs: 22 | repository: 23 | type: string 24 | required: true 25 | description: "The repository of the package to build docs for." 26 | package_name: 27 | type: string 28 | required: true 29 | description: "The name of the package to build docs for." 30 | modules: 31 | type: string 32 | required: true 33 | description: "The modules in the package to build docs for." 34 | pathsToInvalidate: 35 | type: string 36 | required: true 37 | description: "The paths to invalidate in CloudFront, e.g. '/vapor/* /xctvapor/*'." 38 | env: 39 | INPUT_REPOSITORY: ${{ inputs.repository || github.repository }} 40 | INPUT_PACKAGE_NAME: ${{ inputs.package_name }} 41 | INPUT_MODULES: ${{ inputs.modules }} 42 | INPUT_INVALIDATE_PATHS: ${{ inputs.pathsToInvalidate }} 43 | 44 | jobs: 45 | build-docs: 46 | runs-on: ubuntu-22.04 47 | permissions: { id-token: write, contents: read } 48 | env: { AWS_PAGER: '' } 49 | steps: 50 | - name: Check out code 51 | uses: actions/checkout@v4 52 | with: 53 | repository: ${{ inputs.repository || github.repository }} 54 | fetch-depth: 0 55 | - name: Install latest Swift 56 | uses: vapor/swiftly-action@v0.2 57 | with: { toolchain: latest } 58 | - name: Download files 59 | run: | 60 | curl -sL \ 61 | "https://raw.githubusercontent.com/vapor/api-docs/main/generate-package-api-docs.swift" \ 62 | -o generate-package-api-docs.swift \ 63 | "https://raw.githubusercontent.com/vapor/api-docs/main/theme-settings.json" \ 64 | -o theme-settings.json 65 | - name: Build docs 66 | run: 'swift generate-package-api-docs.swift "${INPUT_PACKAGE_NAME}" ${INPUT_MODULES}' 67 | - name: Configure AWS credentials 68 | uses: aws-actions/configure-aws-credentials@v4 69 | with: 70 | role-to-assume: ${{ vars.OIDC_ROLE_ARN }} 71 | aws-region: ${{ vars.OIDC_ROLE_REGION }} 72 | - name: Deploy to S3 73 | env: 74 | S3_BUCKET_URL: ${{ secrets.VAPOR_API_DOCS_S3_BUCKET_URL }} 75 | run: | 76 | aws s3 sync ./public "${S3_BUCKET_URL}" --no-progress --acl public-read 77 | - name: Invalidate CloudFront 78 | env: 79 | DISTRIBUTION_ID: ${{ secrets.VAPOR_API_DOCS_DISTRIBUTION_ID }} 80 | run: | 81 | aws cloudfront create-invalidation --distribution-id "${DISTRIBUTION_ID}" --paths ${INPUT_INVALIDATE_PATHS} 82 | -------------------------------------------------------------------------------- /.github/workflows/deploy-api-docs.yml: -------------------------------------------------------------------------------- 1 | name: API Docs website deploy 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.ref }} 4 | cancel-in-progress: true 5 | on: 6 | push: { branches: [ main ] } 7 | 8 | jobs: 9 | deploy-site: 10 | runs-on: ubuntu-22.04 11 | permissions: { id-token: write, contents: read } 12 | env: { AWS_PAGER: '' } 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v4 16 | - name: Install latest Swift 17 | uses: vapor/swiftly-action@v0.2 18 | with: { toolchain: latest } 19 | - name: Build site 20 | run: swift generate-api-docs.swift 21 | - name: Configure AWS credentials 22 | uses: aws-actions/configure-aws-credentials@v4 23 | with: 24 | role-to-assume: ${{ vars.OIDC_ROLE_ARN }} 25 | aws-region: ${{ vars.OIDC_ROLE_REGION }} 26 | - name: Deploy to CloudFormation 27 | uses: aws-actions/aws-cloudformation-github-deploy@v1 28 | with: 29 | name: vapor-api-docs 30 | template: stack.yaml 31 | no-fail-on-empty-changeset: '1' 32 | parameter-overrides: >- 33 | BucketName=vapor-api-docs-site, 34 | SubDomainName=api, 35 | HostedZoneName=vapor.codes, 36 | AcmCertificateArn=${{ secrets.API_DOCS_CERTIFICATE_ARN }} 37 | - name: Deploy to S3 38 | env: 39 | S3_BUCKET_URL: ${{ secrets.VAPOR_API_DOCS_S3_BUCKET_URL }} 40 | run: 'aws s3 sync ./public "${S3_BUCKET_URL}" --no-progress --acl public-read' 41 | - name: Invalidate CloudFront 42 | env: 43 | DISTRIBUTION_ID: ${{ secrets.VAPOR_API_DOCS_DISTRIBUTION_ID }} 44 | run: 'aws cloudfront create-invalidation --distribution-id "${DISTRIBUTION_ID}" --paths "/*"' 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | code 3 | /packages 4 | /swift-doc 5 | /public 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Qutheory, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | 3 | View the documentation at [api.vapor.codes](http://api.vapor.codes). 4 | 5 | This repo contains the script that generates all vapor api-docs when a new release is cut. 6 | For every repo, on release a github action calls into a script that pulls in the latest version of this repo to the server, then running the provided script. 7 | -------------------------------------------------------------------------------- /api-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vapor/api-docs/735b41fc6f13da3b9de5a62e3e373096b267da14/api-docs.png -------------------------------------------------------------------------------- /error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error Page | Vapor API Docs 5 | 6 | 36 | 37 | 38 |
39 |
40 |

Oops! Something went wrong

41 |
42 |

43 | We couldn't find what you were looking for. Try heading back to the home page. 44 |

45 |
46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /generate-api-docs.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | 3 | import Foundation 4 | 5 | let packages: [String: [String]] = [ 6 | "vapor": ["Vapor", "XCTVapor", "VaporTesting"], 7 | 8 | "async-kit": ["AsyncKit"], 9 | "routing-kit": ["RoutingKit"], 10 | "console-kit": ["ConsoleKit", "ConsoleKitCommands", "ConsoleKitTerminal"], 11 | "websocket-kit": ["WebSocketKit"], 12 | "multipart-kit": ["MultipartKit"], 13 | 14 | "postgres-nio": ["PostgresNIO"], 15 | "mysql-nio": ["MySQLNIO"], 16 | "sqlite-nio": ["SQLiteNIO"], 17 | 18 | "sql-kit": ["SQLKit"], 19 | "postgres-kit": ["PostgresKit"], 20 | "mysql-kit": ["MySQLKit"], 21 | "sqlite-kit": ["SQLiteKit"], 22 | 23 | "fluent-kit": ["FluentKit", "FluentSQL", "XCTFluent"], 24 | "fluent": ["Fluent"], 25 | "fluent-postgres-driver": ["FluentPostgresDriver"], 26 | "fluent-mongo-driver": ["FluentMongoDriver"], 27 | "fluent-mysql-driver": ["FluentMySQLDriver"], 28 | "fluent-sqlite-driver": ["FluentSQLiteDriver"], 29 | 30 | "redis": ["Redis"], 31 | "queues-redis-driver": ["QueuesRedisDriver"], 32 | "queues": ["Queues", "XCTQueues"], 33 | 34 | "leaf-kit": ["LeafKit"], 35 | "leaf": ["Leaf"], 36 | 37 | "jwt-kit": ["JWTKit"], 38 | "jwt": ["JWT"], 39 | "apns": ["VaporAPNS"], 40 | ] 41 | 42 | let htmlMenu = packages.values.flatMap { $0 } 43 | .sorted() 44 | .map { "" } 45 | .joined(separator: "\n") 46 | 47 | do { 48 | let publicDirUrl = URL(fileURLWithPath: "./public", isDirectory: true) 49 | try FileManager.default.removeItemIfExists(at: publicDirUrl) 50 | try FileManager.default.createDirectory(at: publicDirUrl, withIntermediateDirectories: true) 51 | 52 | var htmlIndex = try String(contentsOf: URL(fileURLWithPath: "./index.html", isDirectory: false), encoding: .utf8) 53 | htmlIndex.replace("{{Options}}", with: "\(htmlMenu)\n", maxReplacements: 1) 54 | 55 | try htmlIndex.write(to: publicDirUrl.appendingPathComponent("index.html", isDirectory: false), atomically: true, encoding: .utf8) 56 | try FileManager.default.copyItem(at: URL(fileURLWithPath: "./api-docs.png", isDirectory: false), into: publicDirUrl) 57 | try FileManager.default.copyItem(at: URL(fileURLWithPath: "./error.html", isDirectory: false), into: publicDirUrl) 58 | } catch let error as NSError { 59 | print("❌ ERROR: \(String(reflecting: error)): \(error.userInfo)") 60 | exit(1) 61 | } 62 | 63 | extension NSError { 64 | func isCocoaError(_ code: CocoaError.Code) -> Bool { 65 | self.domain == CocoaError.errorDomain && self.code == code.rawValue 66 | } 67 | } 68 | 69 | extension FileManager { 70 | func removeItemIfExists(at url: URL) throws { 71 | do { 72 | try self.removeItem(at: url) 73 | } catch let error as NSError where error.isCocoaError(.fileNoSuchFile) { 74 | // ignore 75 | } 76 | } 77 | 78 | func copyItem(at src: URL, into dst: URL) throws { 79 | assert(dst.hasDirectoryPath) 80 | 81 | let dstItem = dst.appendingPathComponent(src.lastPathComponent, isDirectory: dst.hasDirectoryPath) 82 | try self.copyItem(at: src, to: dstItem) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /generate-package-api-docs.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | guard CommandLine.argc == 3 else { 4 | print("❌ ERROR: You must provide the package name and modules as a comma separated string.") 5 | exit(1) 6 | } 7 | 8 | let packageName = CommandLine.arguments[1] 9 | let moduleList = CommandLine.arguments[2] 10 | 11 | let modules = moduleList.components(separatedBy: ",") 12 | 13 | let publicDirectoryUrl = URL.currentDirectory().appending(component: "public") 14 | 15 | do { 16 | try run() 17 | } catch { 18 | print("❌ ERROR: \(error).") 19 | exit(1) 20 | } 21 | 22 | // MARK: Functions 23 | 24 | func run() throws { 25 | // Set up 26 | print("⚙️ Ensuring public directory...") 27 | try FileManager.default.removeItemIfExists(at: publicDirectoryUrl) 28 | try FileManager.default.createDirectory(at: publicDirectoryUrl, withIntermediateDirectories: true) 29 | 30 | print("⚙️ Ensuring plugin...") 31 | try ensurePluginAvailable() 32 | 33 | // Run 34 | for module in modules { 35 | print("⚙️ Generating api-docs for package: \(packageName), module: \(module)") 36 | try generateDocs(module: module) 37 | } 38 | 39 | print("✅ Finished generating api-docs for package: \(packageName)") 40 | } 41 | 42 | func ensurePluginAvailable() throws { 43 | var foundAtLeastOne = false 44 | for manifestName in ["6.2", "6.1", "6.0", "5.10", "5.9", "5.8", "5.7"].map({ "Package@swift-\($0).swift" }) + ["Package.swift"] { 45 | print("⚙️ Checking for manifest \(manifestName)") 46 | let manifestUrl = URL.currentDirectory().appending(component: manifestName) 47 | var manifestContents: String 48 | do { manifestContents = try String(contentsOf: manifestUrl, encoding: .utf8) } 49 | catch let error as NSError where error.isCocoaError(.fileReadNoSuchFile) { continue } 50 | catch let error as NSError where error.isPOSIXError(.ENOENT) { continue } 51 | 52 | if !manifestContents.contains(".package(url: \"https://github.com/apple/swift-docc-plugin") { 53 | // This is freely admitted to be quick and dirty. Unfortunately, swift package add-dependency doesn't understand version-tagged manifests. 54 | print("🧬 Injecting DocC plugin dependency into \(manifestName)") 55 | guard let depsArrayRange = manifestContents.firstRange(of: "dependencies: [") else { 56 | print("❌ ERROR: Can't inject swift-docc-plugin dependency (can't find deps array).") 57 | exit(1) 58 | } 59 | manifestContents.insert( 60 | contentsOf: "\n.package(url: \"https://github.com/apple/swift-docc-plugin.git\", from: \"1.4.0\"),\n", 61 | at: depsArrayRange.upperBound 62 | ) 63 | try manifestContents.write(to: manifestUrl, atomically: true, encoding: .utf8) 64 | } 65 | foundAtLeastOne = true 66 | } 67 | guard foundAtLeastOne else { 68 | print("❌ ERROR: Can't inject swift-docc-plugin dependency (no usable manifest found).") 69 | exit(1) 70 | } 71 | } 72 | 73 | func generateDocs(module: String) throws { 74 | print("🔎 Finding DocC catalog") 75 | let doccCatalogs = try FileManager.default.contentsOfDirectory( 76 | at: URL.currentDirectory().appending(components: "Sources", "\(module)"), 77 | includingPropertiesForKeys: nil, 78 | options: [.skipsSubdirectoryDescendants] 79 | ).filter { $0.hasDirectoryPath && $0.pathExtension == "docc" } 80 | guard !doccCatalogs.isEmpty else { 81 | print("❌ ERROR: No DocC catalog found for \(module)") 82 | exit(1) 83 | } 84 | guard doccCatalogs.count == 1 else { 85 | print("❌ ERROR: More than one DocC catalog found for \(module):\n\(doccCatalogs.map(\.lastPathComponent))") 86 | exit(1) 87 | } 88 | let doccCatalogUrl = doccCatalogs[0] 89 | print("🗂️ Using DocC catalog \(doccCatalogUrl.lastPathComponent)") 90 | 91 | print("📐 Copying theme") 92 | try FileManager.default.copyItemIfExistsWithoutOverwrite( 93 | at: URL.currentDirectory().appending(component: "theme-settings.json"), 94 | to: doccCatalogUrl.appending(component: "theme-settings.json") 95 | ) 96 | 97 | print("📝 Generating docs") 98 | try shell([ 99 | "swift", "package", 100 | "--allow-writing-to-directory", publicDirectoryUrl.path, 101 | "generate-documentation", 102 | "--target", module, 103 | "--disable-indexing", 104 | "--experimental-skip-synthesized-symbols", 105 | "--enable-inherited-docs", 106 | "--enable-experimental-overloaded-symbol-presentation", 107 | "--enable-experimental-mentioned-in", 108 | "--hosting-base-path", "/\(module.lowercased())", 109 | "--output-path", publicDirectoryUrl.appending(component: "\(module.lowercased())").path, 110 | ]) 111 | } 112 | 113 | func shell(_ args: String...) throws { try shell(args) } 114 | func shell(_ args: [String]) throws { 115 | // For fun, echo the command: 116 | var sawXOpt = false, seenOpt = false, lastWasOpt = false 117 | print("+ /usr/bin/env \\\n ", terminator: "") 118 | for arg in (args.dropLast() + [args.last! + "\n"]) { 119 | if (seenOpt && !lastWasOpt) || ((!seenOpt || (lastWasOpt && !sawXOpt)) && arg.starts(with: "-")) { 120 | print(" \\\n ", terminator: "") 121 | } 122 | print(" \(arg)", terminator: "") 123 | lastWasOpt = arg.starts(with: "-") 124 | (seenOpt, sawXOpt) = (seenOpt || lastWasOpt, arg.starts(with: "-X")) 125 | } 126 | 127 | // Run the command: 128 | let task = try Process.run(URL(filePath: "/usr/bin/env"), arguments: args) 129 | task.waitUntilExit() 130 | guard task.terminationStatus == 0 else { 131 | throw ShellError(terminationStatus: task.terminationStatus) 132 | } 133 | } 134 | 135 | struct ShellError: Error { 136 | var terminationStatus: Int32 137 | } 138 | 139 | extension FileManager { 140 | func removeItemIfExists(at url: URL) throws { 141 | do { 142 | try self.removeItem(at: url) 143 | } catch let error as NSError where error.isCocoaError(.fileNoSuchFile) { 144 | // ignore 145 | } 146 | } 147 | 148 | func copyItemIfExistsWithoutOverwrite(at src: URL, to dst: URL) throws { 149 | do { 150 | // https://github.com/apple/swift-corelibs-foundation/pull/4808 151 | #if !canImport(Darwin) 152 | do { 153 | _ = try dst.checkResourceIsReachable() 154 | throw NSError(domain: CocoaError.errorDomain, code: CocoaError.fileWriteFileExists.rawValue) 155 | } catch let error as NSError where error.isCocoaError(.fileReadNoSuchFile) {} 156 | #endif 157 | try self.copyItem(at: src, to: dst) 158 | } catch let error as NSError where error.isCocoaError(.fileReadNoSuchFile) { 159 | // ignore 160 | } catch let error as NSError where error.isCocoaError(.fileWriteFileExists) { 161 | // ignore 162 | } 163 | } 164 | } 165 | 166 | extension NSError { 167 | func isCocoaError(_ code: CocoaError.Code) -> Bool { 168 | self.domain == CocoaError.errorDomain && self.code == code.rawValue 169 | } 170 | func isPOSIXError(_ code: POSIXError.Code) -> Bool { 171 | self.domain == POSIXError.errorDomain && self.code == code.rawValue 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /images/article.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/collection.svg: -------------------------------------------------------------------------------- 1 | 2 | Collection 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/curly-brackets.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/technology.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vapor API Docs 5 | 6 | 36 | 37 | 38 |
39 |
40 |

Vapor API Docs

41 |
42 | 46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: 'Create Create a static website on S3 served by CloudFront' 3 | Parameters: 4 | BucketName: 5 | Type: String 6 | Description: The S3 bucket name 7 | SubDomainName: 8 | Type: String 9 | Description: The sub domain name for the site 10 | AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?