Skip to content

Commit 246b0b6

Browse files
committed
wip: imrpove typed-asset generation
1 parent 1763072 commit 246b0b6

26 files changed

+190
-97
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -
4242

4343
# Copy any resources from the public directory and views directory if the directories exist
4444
# Ensure that by default, neither the directory nor any of its contents are writable.
45-
RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
45+
# RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
4646

4747
# ================================
4848
# Run image

Package.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ let package = Package(
3030
.target(name: "AssetGenCLI")
3131
]
3232
),
33+
.target(
34+
name: "PublicAssets",
35+
dependencies: [
36+
.product(name: "Dependencies", package: "swift-dependencies"),
37+
.product(name: "DependenciesMacros", package: "swift-dependencies"),
38+
],
39+
resources: [.copy("assets")],
40+
plugins: ["AssetGenPlugin"]
41+
),
3342
.target(
3443
name: "Models",
3544
dependencies: [
@@ -59,13 +68,13 @@ let package = Package(
5968
dependencies: [
6069
"Models",
6170
"ActivityClient",
71+
"PublicAssets",
6272
.product(name: "Dependencies", package: "swift-dependencies"),
6373
.product(name: "Elementary", package: "elementary"),
6474
.product(name: "Hummingbird", package: "hummingbird"),
6575
.product(name: "Cascadia", package: "swift-cascadia"),
6676
.product(name: "Markdown", package: "swift-markdown")
67-
],
68-
plugins: ["AssetGenPlugin"]
77+
]
6978
),
7079

7180
/// Executable
@@ -76,6 +85,7 @@ let package = Package(
7685
"Routes",
7786
"Pages",
7887
"ActivityClient",
88+
"PublicAssets",
7989
.product(name: "Dependencies", package: "swift-dependencies"),
8090
.product(name: "Hummingbird", package: "hummingbird"),
8191
.product(name: "HummingbirdRouter", package: "hummingbird"),

Plugins/AssetGenPlugin/AssetGenPlugin.swift

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,26 @@ import PackagePlugin
33
@main
44
struct AssetGenPlugin: BuildToolPlugin {
55
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
6-
print("Running build tool [AssetGenPlugin]")
7-
let inputPath = context.package.directoryURL.appending(component: "Public")
8-
let outputPath = context.pluginWorkDirectoryURL.appending(component: "PublicAssets.swift", directoryHint: .notDirectory)
6+
Diagnostics.remark("Running build tool [\(Self.self)]")
7+
guard let sourceModule = target.sourceModule else {
8+
Diagnostics.error("Not a source module")
9+
return []
10+
}
11+
12+
let resources = sourceModule.sourceFiles.filter({ $0.type == .resource })
13+
guard !resources.isEmpty else {
14+
Diagnostics.warning("No resources found")
15+
return []
16+
}
17+
18+
let resourcesInput = resources.flatMap { ["--input", $0.url.path()] }
19+
20+
let outputPath = context.pluginWorkDirectoryURL.appending(component: "Generated\(target.name).swift", directoryHint: .notDirectory)
921
return try [
1022
.prebuildCommand(
11-
displayName: "Static Asset Gen",
23+
displayName: "Running Static Asset Gen",
1224
executable: context.tool(named: "AssetGenCLI").url,
13-
arguments: [
14-
"--directory", inputPath.path(),
25+
arguments: resourcesInput + [
1526
"--output", outputPath.path(),
1627
],
1728
outputFilesDirectory: context.pluginWorkDirectoryURL

Sources/App/Middlewares/SiteMiddleware.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import class Foundation.JSONEncoder
44
import Hummingbird
55
import HummingbirdRouter
66
import Pages
7+
import PublicAssets
78
import Routes
89

910
struct SiteMiddleware<Context: RequestContext>: RouterController {
@@ -16,11 +17,12 @@ struct SiteMiddleware<Context: RequestContext>: RouterController {
1617
ReloadBrowserMiddleware()
1718
#endif
1819

19-
FileMiddleware(
20-
"Public",
21-
urlBasePath: publicAssets.basePath,
22-
searchForIndexHtml: false
23-
)
20+
if publicAssets.baseURL.isFileURL {
21+
FileMiddleware(
22+
publicAssets.baseURL.path(),
23+
searchForIndexHtml: false
24+
)
25+
}
2426

2527
URLRoutingMiddleware(self.siteRouter) { req, ctx, route in
2628
try withDependencies {

Sources/AssetGenCLI/AssetGenCLI.swift

Lines changed: 121 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ import Foundation
33

44
@main
55
struct AssetGenCLI: ParsableCommand {
6-
@Option(help: "Directory containing all files it should generate static")
7-
var directory: String
6+
@Option(name: [.customLong("input")], help: "Directory containing all files it should generate static")
7+
var inputs: [String]
88

99
@Option(help: "The path where the generated output will be created")
1010
var output: String
1111

1212
func run() throws {
13-
let dir = URL(filePath: directory, directoryHint: .isDirectory)
1413
let outFile = URL(filePath: output, directoryHint: .notDirectory)
1514

1615
let fileName = outFile.deletingPathExtension().lastPathComponent
@@ -20,99 +19,155 @@ struct AssetGenCLI: ParsableCommand {
2019
throw Error.swiftExtensionNotInOutfile
2120
}
2221

23-
let items = Self.recursive(dir)
22+
let items = inputs.map { Self.recursive(URL(filePath: $0, directoryHint: .checkFileSystem)) }
2423

2524
try """
25+
import Foundation
2626
public struct \(fileName.pascalCase()): Swift.Sendable {
27-
public let basePath: String
28-
public init(_ basePath: String = "/") {
29-
self.basePath = basePath
27+
public let baseURL: URL
28+
public init() {
29+
self.baseURL = Bundle.module.bundleURL
30+
}
31+
public init(_ baseURL: URL) {
32+
self.baseURL = baseURL
33+
}
34+
\(items.map { $0.code(isFirstLevel: true) }.joined(separator: "\n"), indent: 2)
35+
public protocol File {
36+
var name: String { get }
37+
var ext: String? { get }
38+
var url: URL { get }
3039
}
31-
\(items.map { $0.code() }.joined(separator: "\n"))
3240
public struct AnyFile: Swift.Sendable {
3341
public let name: String
34-
public let ext: String
35-
public let path: String
42+
public let ext: String?
43+
public let url: URL
3644
}
3745
public struct ImageFile: Swift.Sendable {
3846
public let name: String
39-
public let ext: String
40-
public let path: String
41-
public let mimeType = ""
47+
public let ext: String?
48+
public let url: URL
49+
public let width: Int?
50+
public let height: Int?
4251
}
4352
public struct VideoFile: Swift.Sendable {
4453
public let name: String
45-
public let ext: String
46-
public let path: String
47-
public let width: Int = 0
48-
public let height: Int = 0
49-
public let format: String = ""
50-
public let mimeType = ""
54+
public let ext: String?
55+
public let url: URL
56+
public let width: Int?
57+
public let height: Int?
58+
public let mime: String
5159
}
5260
}
5361
"""
5462
.write(to: outFile, atomically: true, encoding: .utf8)
5563

56-
print("Successfully parsed '\(dir)' and wrote to '\(outFile)'")
64+
print("Successfully parsed '\(inputs)' directory and generated to '\(output)'")
5765
}
5866

5967
private enum Error: Swift.Error {
6068
case swiftExtensionNotInOutfile
6169
}
6270

63-
private static func recursive(_ dir: URL) -> [FileOrDir] {
64-
guard let enumerator = try? FileManager.default.contentsOfDirectory(
65-
at: dir,
66-
includingPropertiesForKeys: [.nameKey, .isDirectoryKey],
67-
options: [.skipsHiddenFiles]
68-
) else {
69-
return []
70-
}
71-
72-
return enumerator.compactMap { url in
73-
let resourceValues = try? url.resourceValues(forKeys: [.isDirectoryKey])
74-
let isDirectory = resourceValues?.isDirectory ?? false
75-
76-
if isDirectory {
77-
return .dir(
78-
canonical: url.lastPathComponent,
79-
recursive(url)
80-
)
81-
} else {
82-
return .file(
83-
canonical: url.deletingPathExtension().lastPathComponent,
84-
ext: url.pathExtension
85-
)
71+
private static func recursive(_ url: URL) -> FileOrDir {
72+
var isDirectory = false
73+
_ = FileManager.default.fileExists(atPath: url.path(), isDirectory: &isDirectory)
74+
75+
if isDirectory {
76+
guard let enumerator = try? FileManager.default.contentsOfDirectory(
77+
at: url,
78+
includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey],
79+
options: [.skipsHiddenFiles]
80+
) else {
81+
return .dir(canonical: url.lastPathComponent, [])
8682
}
83+
84+
return .dir(
85+
canonical: url.lastPathComponent,
86+
enumerator.compactMap(Self.recursive)
87+
)
88+
} else {
89+
return .file(
90+
canonical: url.deletingPathExtension().lastPathComponent,
91+
ext: url.pathExtension.isEmpty ? nil : url.pathExtension,
92+
type: .from(ext: url.pathExtension)
93+
)
8794
}
8895
}
8996

9097
private enum FileOrDir {
9198
case dir(canonical: String, [Self])
92-
case file(canonical: String, ext: String)
99+
case file(canonical: String, ext: String?, type: FileType)
93100

94-
func code(_ indent: Int = 2, path: [String] = []) -> String {
101+
func code(path: [String] = [], isFirstLevel: Bool = false) -> String {
95102
switch self {
96103
case let .dir(canonical, items):
97104
"""
98-
\(String(repeating: " ", count: indent))public var `\(canonical.camelCase())`: \(canonical.pascalCase()) {
99-
\(String(repeating: " ", count: indent)) \(canonical.pascalCase())(basePath: self.basePath)
100-
\(String(repeating: " ", count: indent))}
101-
\(String(repeating: " ", count: indent))public struct \(canonical.pascalCase()): Swift.Sendable {
102-
\(String(repeating: " ", count: indent)) fileprivate let basePath: String
103-
\(items.map { $0.code(indent + 2, path: path + [canonical]) }.joined(separator: "\n"))
104-
\(String(repeating: " ", count: indent))}
105+
public var `\(canonical.camelCase())`: \(canonical.pascalCase()) {
106+
\(canonical.pascalCase())(baseURL: \(isFirstLevel ? "URL(filePath: \"\(canonical)\", directoryHint: .isDirectory, relativeTo: self.baseURL)" : "self.baseURL.appending(path: \"\(canonical)\", directoryHint: .isDirectory)"))
107+
}
108+
public struct \(canonical.pascalCase()): Swift.Sendable {
109+
public let baseURL: URL
110+
\(items.map { $0.code(path: path + [canonical]) }.joined(separator: "\n"), indent: 2)
111+
}
112+
"""
113+
case let .file(canonical, ext, type):
114+
switch type {
115+
case .unknown:
116+
"""
117+
public var `\(canonical.camelCase())`: AnyFile {
118+
.init(
119+
name: "\(canonical)",
120+
ext: \(ext.flatMap { "\"\($0)\"" } ?? "nil"),
121+
url: self.baseURL.appending(path: "\(canonical)", directoryHint: .notDirectory)\(ext.flatMap { ".appendingPathExtension(\"\($0)\")" } ?? "")
122+
)
123+
}
124+
"""
125+
case .image(let width, let height):
126+
"""
127+
public var `\(canonical.camelCase())`: ImageFile {
128+
.init(
129+
name: "\(canonical)",
130+
ext: \(ext.flatMap { "\"\($0)\"" } ?? "nil"),
131+
url: self.baseURL.appending(path: "\(canonical)", directoryHint: .notDirectory)\(ext.flatMap { ".appendingPathExtension(\"\($0)\")" } ?? ""),
132+
width: \(width.flatMap(String.init) ?? "nil"),
133+
height: \(height.flatMap(String.init) ?? "nil")
134+
)
135+
}
105136
"""
106-
case let .file(canonical, ext):
137+
case let .video(width, height, mime):
107138
"""
108-
\(String(repeating: " ", count: indent))public var `\(canonical.camelCase())`: AnyFile {
109-
\(String(repeating: " ", count: indent)) AnyFile(
110-
\(String(repeating: " ", count: indent)) name: "\(canonical)",
111-
\(String(repeating: " ", count: indent)) ext: "\(ext)",
112-
\(String(repeating: " ", count: indent)) path: "\\(self.basePath)/\((path + ["\(canonical).\(ext)"]).joined(separator: "/"))"
113-
\(String(repeating: " ", count: indent)) )
114-
\(String(repeating: " ", count: indent))}
139+
public var `\(canonical.camelCase())`: VideoFile {
140+
.init(
141+
name: "\(canonical)",
142+
ext: \(ext.flatMap { "\"\($0)\"" } ?? "nil"),
143+
url: self.baseURL.appending(path: "\(canonical)", directoryHint: .notDirectory)\(ext.flatMap { ".appendingPathExtension(\"\($0)\")" } ?? ""),
144+
width: \(width.flatMap(String.init) ?? "nil"),
145+
height: \(height.flatMap(String.init) ?? "nil"),
146+
mime: "\(mime)"
147+
)
148+
}
115149
"""
150+
}
151+
}
152+
}
153+
154+
enum FileType {
155+
case image(width: Int?, height: Int?)
156+
case video(width: Int?, height: Int?, mime: String)
157+
case unknown
158+
159+
static func from(ext: String) -> Self {
160+
switch ext.trimmingCharacters(in: .whitespacesAndNewlines) {
161+
case "gif": .image(width: nil, height: nil)
162+
case "jpeg", "jpg": .image(width: nil, height: nil)
163+
case "svg": .image(width: nil, height: nil)
164+
case "webp": .image(width: nil, height: nil)
165+
case "png": .image(width: nil, height: nil)
166+
case "mp4": .video(width: nil, height: nil, mime: "video/mp4")
167+
case "mov": .video(width: nil, height: nil, mime: "video/quicktime")
168+
case "webm": .video(width: nil, height: nil, mime: "video/webm")
169+
default: .unknown
170+
}
116171
}
117172
}
118173
}
@@ -150,4 +205,10 @@ private extension String {
150205
}
151206
.joined()
152207
}
208+
}
209+
210+
private extension String.StringInterpolation {
211+
mutating func appendInterpolation<S: StringProtocol>(_ value: S, indent: Int) {
212+
appendInterpolation(value.components(separatedBy: "\n").joined(separator: "\n\(String(repeating: " ", count: indent))"))
213+
}
153214
}

Sources/Pages/HomePage.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,9 @@ public struct HomePage: Page {
217217
switch postHeader {
218218
case let .link(link):
219219
EmptyHTML()
220-
case let .image(src, label):
221-
img(.src(src), .class("post__header"), .custom(name: "alt", value: label), .aria.label(label))
222-
case let .video(src):
220+
case let .image(asset, label):
221+
img(.src(asset.url.assetString), .class("post__header"), .custom(name: "alt", value: label), .aria.label(label))
222+
case let .video(asset):
223223
video(
224224
.class("post__header"),
225225
.custom(name: "autoplay", value: ""),
@@ -228,7 +228,7 @@ public struct HomePage: Page {
228228
.custom(name: "controls", value: ""),
229229
.custom(name: "loop", value: "")
230230
) {
231-
source(.src(src))
231+
source(.src(asset.url.assetString), .custom(name: "type", value: asset.mime))
232232
"Your browser does not support playing this video"
233233
}
234234
case let .code(rawCode, lang):

Sources/Pages/Models/Post+AllCases.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Dependencies
3+
import PublicAssets
34

45
extension Post: CaseIterable {
56
static var allCases: [Self] {
@@ -17,7 +18,7 @@ extension Post: CaseIterable {
1718
),
1819
Self(
1920
id: 2,
20-
header: .image(assets.projects.animeNow.anDiscover.path, label: "Anime Now! discover image"),
21+
header: .image(assets.assets.projects.animeNow.anDiscover, label: "Anime Now! discover image"),
2122
title: "Anime Now! \u{2014} An iOS and macOS App",
2223
content: """
2324
> TBD

0 commit comments

Comments
 (0)