├── .gitignore ├── .spi.yml ├── Package.resolved ├── Package.swift ├── README.md └── Sources └── ComputeUI ├── ComputeUI.swift ├── GoogleAnalytics.swift ├── Router+View.swift └── URL+Path.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [ComputeUI] 5 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "compute", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/swift-cloud/Compute", 7 | "state" : { 8 | "revision" : "d3931075db2e08c044aaf28e293c855eea7eb226", 9 | "version" : "2.8.0" 10 | } 11 | }, 12 | { 13 | "identity" : "crypto", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/swift-cloud/Crypto", 16 | "state" : { 17 | "revision" : "defff250bbd79d81fb8ee53926360fc522d75bad", 18 | "version" : "1.6.0" 19 | } 20 | }, 21 | { 22 | "identity" : "javascriptkit", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/swiftwasm/JavaScriptKit.git", 25 | "state" : { 26 | "revision" : "dac9d7b0342c5027fc74c8d2f212f10624123a7b", 27 | "version" : "0.17.0" 28 | } 29 | }, 30 | { 31 | "identity" : "opencombine", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/OpenCombine/OpenCombine.git", 34 | "state" : { 35 | "revision" : "9cf67e363738dbab61b47fb5eaed78d3db31e5ee", 36 | "version" : "0.13.0" 37 | } 38 | }, 39 | { 40 | "identity" : "opencombinejs", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/swiftwasm/OpenCombineJS.git", 43 | "state" : { 44 | "revision" : "e574e418ba468ff5c2d4c499eb56f108aeb4d2ba", 45 | "version" : "0.2.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-argument-parser", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-argument-parser", 52 | "state" : { 53 | "revision" : "fddd1c00396eed152c45a46bea9f47b98e59301d", 54 | "version" : "1.2.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-benchmark", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/google/swift-benchmark", 61 | "state" : { 62 | "revision" : "8163295f6fe82356b0bcf8e1ab991645de17d096", 63 | "version" : "0.1.2" 64 | } 65 | }, 66 | { 67 | "identity" : "tokamak", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/TokamakUI/Tokamak", 70 | "state" : { 71 | "revision" : "f1cbfcf073e2675566b0e9aa337441357d40d88a", 72 | "version" : "0.11.1" 73 | } 74 | } 75 | ], 76 | "version" : 2 77 | } 78 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ComputeUI", 7 | platforms: [ 8 | .macOS(.v11), 9 | .iOS(.v13), 10 | .tvOS(.v13), 11 | .watchOS(.v9), 12 | .driverKit(.v22), 13 | .macCatalyst(.v13) 14 | ], 15 | products: [ 16 | .library(name: "ComputeUI", targets: ["ComputeUI"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/swift-cloud/Compute", from: "2.8.0"), 20 | .package(url: "https://github.com/TokamakUI/Tokamak", from: "0.11.1") 21 | ], 22 | targets: [ 23 | .target(name: "ComputeUI", dependencies: [ 24 | "Compute", 25 | .product(name: "TokamakStaticHTML", package: "Tokamak") 26 | ]) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComputeUI 2 | 3 | Build server side rendered webpages in SwiftUI 4 | 5 | ```swift 6 | import ComputeUI 7 | 8 | struct IndexPage: View { 9 | 10 | @Environment(\.request) var req 11 | 12 | var body: some View { 13 | VStack { 14 | Text("Hello, Swift") 15 | .font(.title) 16 | 17 | Text("This is a server rendered SwiftUI website") 18 | .font(.subheadline) 19 | 20 | Text("Your ip address \(req.clientIpAddress().stringValue)") 21 | } 22 | } 23 | } 24 | 25 | try await Router() 26 | .get("/", IndexPage()) 27 | .listen() 28 | ``` 29 | -------------------------------------------------------------------------------- /Sources/ComputeUI/ComputeUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComputeUI.swift 3 | // 4 | // 5 | // Created by Andrew Barba on 11/28/22. 6 | // 7 | 8 | @_exported import Compute 9 | @_exported import TokamakCore 10 | @_exported import TokamakStaticHTML 11 | -------------------------------------------------------------------------------- /Sources/ComputeUI/GoogleAnalytics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GoogleAnalytics.swift 3 | // 4 | // 5 | // Created by Andrew Barba on 11/28/22. 6 | // 7 | 8 | import TokamakStaticHTML 9 | 10 | public struct GoogleAnalytics: View { 11 | 12 | public let id: String 13 | 14 | public init(id: String) { 15 | self.id = id 16 | } 17 | 18 | public var body: some View { 19 | Group { 20 | HTML("script", [ 21 | "async": "true", 22 | "src": "https://www.googletagmanager.com/gtag/js?id=\(id)" 23 | ]) 24 | HTML("script", content: 25 | """ 26 | window.dataLayer = window.dataLayer || []; 27 | function gtag(){dataLayer.push(arguments);} 28 | gtag('js', new Date()); 29 | gtag('config', '\(id)'); 30 | """ 31 | ) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ComputeUI/Router+View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router+View.swift 3 | // 4 | // 5 | // Created by Andrew Barba on 11/27/22. 6 | // 7 | 8 | extension Router { 9 | 10 | public typealias ViewHandler = (IncomingRequest, OutgoingResponse) async throws -> T 11 | 12 | @discardableResult 13 | public func get(_ path: String, _ handler: @autoclosure @escaping () -> T) -> Self { 14 | return get(path) { _, _ in handler() } 15 | } 16 | 17 | @discardableResult 18 | public func get(_ path: String, _ handler: @escaping ViewHandler) -> Self { 19 | return get(path, render(handler)) 20 | } 21 | 22 | @discardableResult 23 | public func post(_ path: String, _ handler: @autoclosure @escaping () -> T) -> Self { 24 | return post(path) { _, _ in handler() } 25 | } 26 | 27 | @discardableResult 28 | public func post(_ path: String, _ handler: @escaping ViewHandler) -> Self { 29 | return post(path, render(handler)) 30 | } 31 | 32 | private func render(_ handler: @escaping ViewHandler) -> Router.Handler { 33 | return { req, res in 34 | RequestKey.defaultValue = req 35 | ResponseKey.defaultValue = res 36 | let view = try await handler(req, res) 37 | .environment(\.request, req) 38 | .environment(\.response, res) 39 | let html = StaticHTMLRenderer(view).render() 40 | try await res 41 | .status(.ok) 42 | .upgradeToHTTP3() 43 | .compress() 44 | .send(html: html) 45 | } 46 | } 47 | } 48 | 49 | // MARK: - Request Environment 50 | 51 | private struct RequestKey: EnvironmentKey { 52 | static var defaultValue: Compute.IncomingRequest? 53 | } 54 | 55 | extension EnvironmentValues { 56 | public var request: IncomingRequest { 57 | get { self[RequestKey.self] ?? RequestKey.defaultValue! } 58 | set { self[RequestKey.self] = newValue } 59 | } 60 | } 61 | 62 | // MARK: - Response Environment 63 | 64 | private struct ResponseKey: EnvironmentKey { 65 | static var defaultValue: Compute.OutgoingResponse? 66 | } 67 | 68 | extension EnvironmentValues { 69 | public var response: OutgoingResponse { 70 | get { self[ResponseKey.self] ?? ResponseKey.defaultValue! } 71 | set { self[ResponseKey.self] = newValue } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/ComputeUI/URL+Path.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Path.swift 3 | // 4 | // 5 | // Created by Andrew Barba on 11/28/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URL { 11 | 12 | public static func path(_ location: String) -> URL { 13 | return .init(string: location.starts(with: "/") ? location : "/\(location)")! 14 | } 15 | } 16 | --------------------------------------------------------------------------------