├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── deploy-api-docs.yml │ └── build-and-deploy-docs-workflow.yml ├── .gitignore ├── api-docs.png ├── README.md ├── error.html ├── images ├── collection.svg ├── curly-brackets.svg ├── article.svg └── technology.svg ├── index.html ├── LICENSE ├── theme-settings.json ├── styles.css ├── stack.yaml ├── generate-api-docs.swift └── generate-package-api-docs.swift /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @0xTim @gwynne 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | code 3 | /packages 4 | /swift-doc 5 | /public 6 | -------------------------------------------------------------------------------- /api-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vapor/api-docs/HEAD/api-docs.png -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error Page | Vapor API Docs 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Oops! Something went wrong

12 |
13 |

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

16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /images/collection.svg: -------------------------------------------------------------------------------- 1 | 2 | Collection 3 | 4 | 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vapor API Docs 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Vapor API Docs

13 |

Explore the complete API documentation for Vapor and its ecosystem

14 |
15 | 16 |
17 | {{PackageCards}} 18 |
19 | 20 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /images/curly-brackets.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /images/article.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /theme-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "aside": { 4 | "border-radius": "16px", 5 | "border-style": "double", 6 | "border-width": "3px" 7 | }, 8 | "border-radius": "0", 9 | "button": { 10 | "border-radius": "16px", 11 | "border-width": "1px", 12 | "border-style": "solid" 13 | }, 14 | "code": { 15 | "border-radius": "16px", 16 | "border-width": "1px", 17 | "border-style": "solid" 18 | }, 19 | "color": { 20 | "fill": { 21 | "dark": "rgb(0, 0, 0)", 22 | "light": "rgb(255, 255, 255)" 23 | }, 24 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-documentation-intro-accent) 30%, #000 100%)", 25 | "documentation-intro-accent": "rgb(204, 204, 204)", 26 | "hero-eyebrow": "white", 27 | "documentation-intro-figure": "white", 28 | "hero-title": "white", 29 | "logo-base": { "dark": "#fff", "light": "#000" }, 30 | "logo-shape": { "dark": "#000", "light": "#fff" } 31 | }, 32 | "icons": { 33 | "article": "/images/article.svg", 34 | "collection": "/images/collection.svg", 35 | "curly-brackets": "/images/curly-brackets.svg" 36 | } 37 | }, 38 | "features": { 39 | "quickNavigation": { 40 | "enable": true 41 | }, 42 | "i18n": { 43 | "enable": true 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.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@v6 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@v5 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 | -------------------------------------------------------------------------------- /images/technology.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.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-latest 47 | permissions: { id-token: write, contents: read } 48 | env: { AWS_PAGER: '' } 49 | steps: 50 | - name: Check out docc-render fork 51 | uses: actions/checkout@v6 52 | with: 53 | repository: vapor/swift-docc-render 54 | path: swift-docc-render 55 | - name: Setup node 20 56 | uses: actions/setup-node@v6 57 | with: 58 | node-version: 20 59 | - name: Build swift-docc-render 60 | working-directory: swift-docc-render 61 | run: npm ci && npm run build 62 | - name: Check out repo to build docs for 63 | uses: actions/checkout@v6 64 | with: 65 | repository: ${{ inputs.repository || github.repository }} 66 | path: build-repo 67 | fetch-depth: 0 68 | - name: Install latest Swift 69 | uses: vapor/swiftly-action@v0.2 70 | with: { toolchain: latest } 71 | - name: Download files 72 | run: | 73 | curl -sL \ 74 | "https://raw.githubusercontent.com/vapor/api-docs/main/generate-package-api-docs.swift" \ 75 | -o build-repo/generate-package-api-docs.swift \ 76 | "https://raw.githubusercontent.com/vapor/api-docs/main/theme-settings.json" \ 77 | -o build-repo/theme-settings.json 78 | - name: Build docs 79 | working-directory: build-repo 80 | env: 81 | DOCC_HTML_DIR: '../swift-docc-render/dist' 82 | run: 'swift generate-package-api-docs.swift "${INPUT_PACKAGE_NAME}" ${INPUT_MODULES}' 83 | - name: Configure AWS credentials 84 | uses: aws-actions/configure-aws-credentials@v5 85 | with: 86 | role-to-assume: ${{ vars.OIDC_ROLE_ARN }} 87 | aws-region: ${{ vars.OIDC_ROLE_REGION }} 88 | - name: Deploy to S3 89 | env: 90 | S3_BUCKET_URL: ${{ secrets.VAPOR_API_DOCS_S3_BUCKET_URL }} 91 | run: | 92 | aws s3 sync ./build-repo/public "${S3_BUCKET_URL}" --no-progress --acl public-read 93 | - name: Invalidate CloudFront 94 | env: 95 | DISTRIBUTION_ID: ${{ secrets.VAPOR_API_DOCS_DISTRIBUTION_ID }} 96 | run: | 97 | aws cloudfront create-invalidation --distribution-id "${DISTRIBUTION_ID}" --paths ${INPUT_INVALIDATE_PATHS} 98 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | html, body { 8 | min-height: 100%; 9 | background: #0a0a0a; 10 | color: #ffffff; 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 12 | line-height: 1.6; 13 | } 14 | 15 | body { 16 | background: linear-gradient(135deg, #0a0a0a 0%, #1a0a2e 100%); 17 | background-attachment: fixed; 18 | } 19 | 20 | .container { 21 | max-width: 1200px; 22 | margin: 0 auto; 23 | padding: 2rem; 24 | } 25 | 26 | .header { 27 | text-align: center; 28 | margin-bottom: 3rem; 29 | } 30 | 31 | .logo { 32 | background-image: url(api-docs.png); 33 | width: 300px; 34 | height: 78px; 35 | background-size: contain; 36 | background-repeat: no-repeat; 37 | background-position: center; 38 | text-indent: -9999px; 39 | margin: 0 auto 1rem; 40 | } 41 | 42 | .subtitle { 43 | color: #a0a0a0; 44 | font-size: 1.1rem; 45 | margin-bottom: 2rem; 46 | } 47 | 48 | .package-grid { 49 | display: grid; 50 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 51 | gap: 1.5rem; 52 | margin-bottom: 3rem; 53 | } 54 | 55 | .package-card { 56 | background: rgba(255, 255, 255, 0.05); 57 | border: 1px solid rgba(255, 255, 255, 0.1); 58 | border-radius: 12px; 59 | padding: 1.5rem; 60 | text-decoration: none; 61 | color: inherit; 62 | transition: all 0.3s ease; 63 | cursor: pointer; 64 | position: relative; 65 | overflow: hidden; 66 | } 67 | 68 | .package-card::before { 69 | content: ''; 70 | position: absolute; 71 | top: 0; 72 | left: 0; 73 | right: 0; 74 | height: 3px; 75 | background: linear-gradient(90deg, #7c3aed 0%, #3b82f6 100%); 76 | transform: scaleX(0); 77 | transition: transform 0.3s ease; 78 | } 79 | 80 | .package-card:hover { 81 | background: rgba(255, 255, 255, 0.08); 82 | border-color: rgba(255, 255, 255, 0.2); 83 | transform: translateY(-2px); 84 | box-shadow: 0 8px 24px rgba(124, 58, 237, 0.2); 85 | } 86 | 87 | .package-card:hover::before { 88 | transform: scaleX(1); 89 | } 90 | 91 | .package-name { 92 | font-size: 1.25rem; 93 | font-weight: 600; 94 | margin-bottom: 0.5rem; 95 | color: #ffffff; 96 | } 97 | 98 | .package-description { 99 | font-size: 0.9rem; 100 | color: #a0a0a0; 101 | line-height: 1.5; 102 | } 103 | 104 | .footer { 105 | text-align: center; 106 | margin-top: 4rem; 107 | padding-top: 2rem; 108 | border-top: 1px solid rgba(255, 255, 255, 0.1); 109 | color: #666; 110 | font-size: 0.9rem; 111 | } 112 | 113 | .footer a { 114 | color: #7c3aed; 115 | text-decoration: none; 116 | } 117 | 118 | .footer a:hover { 119 | text-decoration: underline; 120 | } 121 | 122 | /* Responsive design */ 123 | @media (max-width: 768px) { 124 | .container { 125 | padding: 1rem; 126 | } 127 | 128 | .package-grid { 129 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 130 | gap: 1rem; 131 | } 132 | 133 | .logo { 134 | width: 250px; 135 | height: 65px; 136 | } 137 | 138 | .subtitle { 139 | font-size: 1rem; 140 | } 141 | } 142 | 143 | @media (max-width: 480px) { 144 | .package-grid { 145 | grid-template-columns: 1fr; 146 | } 147 | } 148 | 149 | /* Focus styles for accessibility */ 150 | .package-card:focus { 151 | outline: 2px solid #7c3aed; 152 | outline-offset: 2px; 153 | } 154 | 155 | /* Loading animation */ 156 | @keyframes fadeIn { 157 | from { 158 | opacity: 0; 159 | transform: translateY(20px); 160 | } 161 | to { 162 | opacity: 1; 163 | transform: translateY(0); 164 | } 165 | } 166 | 167 | .package-card { 168 | animation: fadeIn 0.5s ease forwards; 169 | opacity: 0; 170 | } 171 | 172 | .package-card:nth-child(1) { animation-delay: 0.05s; } 173 | .package-card:nth-child(2) { animation-delay: 0.1s; } 174 | .package-card:nth-child(3) { animation-delay: 0.15s; } 175 | .package-card:nth-child(4) { animation-delay: 0.2s; } 176 | .package-card:nth-child(5) { animation-delay: 0.25s; } 177 | .package-card:nth-child(6) { animation-delay: 0.3s; } 178 | .package-card:nth-child(7) { animation-delay: 0.35s; } 179 | .package-card:nth-child(8) { animation-delay: 0.4s; } 180 | .package-card:nth-child(n+9) { animation-delay: 0.45s; } -------------------------------------------------------------------------------- /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}(? 88 |

\(module)

89 |

\(description)

90 | 91 | """ 92 | }.joined(separator: "\n") 93 | 94 | do { 95 | let publicDirUrl = URL(fileURLWithPath: "./public", isDirectory: true) 96 | try FileManager.default.removeItemIfExists(at: publicDirUrl) 97 | try FileManager.default.createDirectory(at: publicDirUrl, withIntermediateDirectories: true) 98 | 99 | var htmlIndex = try String(contentsOf: URL(fileURLWithPath: "./index.html", isDirectory: false), encoding: .utf8) 100 | htmlIndex.replace("{{PackageCards}}", with: packageCards, maxReplacements: 1) 101 | 102 | try htmlIndex.write(to: publicDirUrl.appendingPathComponent("index.html", isDirectory: false), atomically: true, encoding: .utf8) 103 | try FileManager.default.copyItem(at: URL(fileURLWithPath: "./api-docs.png", isDirectory: false), into: publicDirUrl) 104 | try FileManager.default.copyItem(at: URL(fileURLWithPath: "./error.html", isDirectory: false), into: publicDirUrl) 105 | try FileManager.default.copyItem(at: URL(fileURLWithPath: "./styles.css", isDirectory: false), into: publicDirUrl) 106 | } catch let error as NSError { 107 | print("❌ ERROR: \(String(reflecting: error)): \(error.userInfo)") 108 | exit(1) 109 | } 110 | 111 | extension NSError { 112 | func isCocoaError(_ code: CocoaError.Code) -> Bool { 113 | self.domain == CocoaError.errorDomain && self.code == code.rawValue 114 | } 115 | } 116 | 117 | extension FileManager { 118 | func removeItemIfExists(at url: URL) throws { 119 | do { 120 | try self.removeItem(at: url) 121 | } catch let error as NSError where error.isCocoaError(.fileNoSuchFile) { 122 | // ignore 123 | } 124 | } 125 | 126 | func copyItem(at src: URL, into dst: URL) throws { 127 | assert(dst.hasDirectoryPath) 128 | 129 | let dstItem = dst.appendingPathComponent(src.lastPathComponent, isDirectory: dst.hasDirectoryPath) 130 | try self.copyItem(at: src, to: dstItem) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------