/// An asset shipped with a website: a file stored under the static files root and served by Hummingbird's `FileMiddleware` middleware. /// /// A conforming asset supplies its file name and the extensions it is available with, each resolving to its own file; the protocol derives the paths from them: /// the file's path within the static files root and the URL path it is served at, optionally versioned to bust caches. Each file lands in its extension's own /// folder unless the asset names a ``folder`` of its own. public protocol Asset: Sendable { // MARK: Properties /// The folder within the static files root that holds the asset's files, or `nil` (the default) to use each extension's own folder. var folder: String? { get } /// The file extensions the asset is available with. var fileExtensions: [AssetExtension] { get } /// The asset's file name, without extension. var fileName: String { get } } // MARK: - Implementations public extension Asset { // MARK: Computed /// The asset's files live in each extension's own folder by default. var folder: String? { nil } // MARK: Methods /// Resolves the asset's path against the given base directory. /// /// - Parameters: /// - basePath: the directory the static files are served from. /// - fileExtension: the extension of the file to resolve. /// - Returns: the path to the file, relative to the `basePath` path. func path( relativeTo basePath: String, for fileExtension: AssetExtension ) -> String { let relativePath = relativePath(for: fileExtension) guard !basePath.isEmpty else { return relativePath } return "\(basePath)/\(relativePath)" } /// Resolves the asset's path relative to the static files root (e.g. `"css/shared.css"`). /// /// This also matches the URL path the file is served at by `FileMiddleware`. /// /// - Parameter fileExtension: the extension of the file to resolve. /// - Returns: the path to the file, relative to the static files root. func relativePath( for fileExtension: AssetExtension ) -> String { let file = "\(fileName).\(fileExtension.rawValue)" return (folder ?? fileExtension.folder) .map { "\($0)/\(file)" } ?? file } /// Resolves the absolute URL path the asset is served at (e.g. `"/css/shared.css"`). /// /// A version token appends as a `v` query parameter (e.g. `"/css/shared.css?v=abc123"`): `FileMiddleware` ignores the query when /// resolving the file, while caches key on the full URL, so a deploy that changes the assets busts every cached copy at once. /// - Parameters: /// - fileExtension: the extension of the file to resolve. /// - version: the version token to append, or `nil` to leave the URL unversioned. /// - Returns: the path to use in `href` and `src` attributes. func urlPath( for fileExtension: AssetExtension, version: String? = nil ) -> String { let path = "/\(relativePath(for: fileExtension))" guard let version, !version.isEmpty else { return path } return "\(path)?v=\(version)" } }