Skip to content

Commit 1763072

Browse files
committed
wip: add plugin that generates statically typed assets for Public folder
1 parent e31e2fe commit 1763072

File tree

8 files changed

+220
-25
lines changed

8 files changed

+220
-25
lines changed

Package.resolved

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:6.0
1+
// swift-tools-version:6.0.3
22

33
import PackageDescription
44

@@ -7,13 +7,6 @@ let package = Package(
77
platforms: [
88
.macOS(.v13),
99
],
10-
products: [
11-
.library(name: "ActivityClient", targets: ["ActivityClient"]),
12-
.library(name: "Models", targets: ["Models"]),
13-
.library(name: "Routes", targets: ["Routes"]),
14-
.library(name: "Pages", targets: ["Pages"]),
15-
.executable(name: "App", targets: ["App"]),
16-
],
1710
dependencies: [
1811
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"),
1912
.package(url: "https://github.com/hummingbird-project/hummingbird.git", exact: "2.5.0"),
@@ -24,6 +17,19 @@ let package = Package(
2417
.package(url: "https://github.com/swiftlang/swift-markdown.git", revision: "e62a44fd1f2764ba8807db3b6f257627449bbb8c")
2518
],
2619
targets: [
20+
.executableTarget(
21+
name: "AssetGenCLI",
22+
dependencies: [
23+
.product(name: "ArgumentParser", package: "swift-argument-parser")
24+
]
25+
),
26+
.plugin(
27+
name: "AssetGenPlugin",
28+
capability: .buildTool(),
29+
dependencies: [
30+
.target(name: "AssetGenCLI")
31+
]
32+
),
2733
.target(
2834
name: "Models",
2935
dependencies: [
@@ -58,7 +64,8 @@ let package = Package(
5864
.product(name: "Hummingbird", package: "hummingbird"),
5965
.product(name: "Cascadia", package: "swift-cascadia"),
6066
.product(name: "Markdown", package: "swift-markdown")
61-
]
67+
],
68+
plugins: ["AssetGenPlugin"]
6269
),
6370

6471
/// Executable
@@ -81,7 +88,7 @@ let package = Package(
8188
)
8289

8390
package.targets
84-
.filter { $0.type != .binary }
91+
.filter { $0.type != .binary && $0.type != .plugin }
8592
.forEach {
8693
$0.swiftSettings = [
8794
.unsafeFlags([
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct AssetGenPlugin: BuildToolPlugin {
5+
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)
9+
return try [
10+
.prebuildCommand(
11+
displayName: "Static Asset Gen",
12+
executable: context.tool(named: "AssetGenCLI").url,
13+
arguments: [
14+
"--directory", inputPath.path(),
15+
"--output", outputPath.path(),
16+
],
17+
outputFilesDirectory: context.pluginWorkDirectoryURL
18+
)
19+
]
20+
}
21+
}

Sources/App/Middlewares/SiteMiddleware.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Routes
99
struct SiteMiddleware<Context: RequestContext>: RouterController {
1010
@Dependency(\.siteRouter) private var siteRouter
1111
@Dependency(\.activityClient) private var activityClient
12+
@Dependency(\.publicAssets) private var publicAssets
1213

1314
var body: some RouterMiddleware<Context> {
1415
#if DEBUG
@@ -17,7 +18,7 @@ struct SiteMiddleware<Context: RequestContext>: RouterController {
1718

1819
FileMiddleware(
1920
"Public",
20-
urlBasePath: "/assets",
21+
urlBasePath: publicAssets.basePath,
2122
searchForIndexHtml: false
2223
)
2324

Sources/AssetGenCLI/AssetGenCLI.swift

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import ArgumentParser
2+
import Foundation
3+
4+
@main
5+
struct AssetGenCLI: ParsableCommand {
6+
@Option(help: "Directory containing all files it should generate static")
7+
var directory: String
8+
9+
@Option(help: "The path where the generated output will be created")
10+
var output: String
11+
12+
func run() throws {
13+
let dir = URL(filePath: directory, directoryHint: .isDirectory)
14+
let outFile = URL(filePath: output, directoryHint: .notDirectory)
15+
16+
let fileName = outFile.deletingPathExtension().lastPathComponent
17+
let fileExt = outFile.pathExtension
18+
19+
guard fileExt == "swift" else {
20+
throw Error.swiftExtensionNotInOutfile
21+
}
22+
23+
let items = Self.recursive(dir)
24+
25+
try """
26+
public struct \(fileName.pascalCase()): Swift.Sendable {
27+
public let basePath: String
28+
public init(_ basePath: String = "/") {
29+
self.basePath = basePath
30+
}
31+
\(items.map { $0.code() }.joined(separator: "\n"))
32+
public struct AnyFile: Swift.Sendable {
33+
public let name: String
34+
public let ext: String
35+
public let path: String
36+
}
37+
public struct ImageFile: Swift.Sendable {
38+
public let name: String
39+
public let ext: String
40+
public let path: String
41+
public let mimeType = ""
42+
}
43+
public struct VideoFile: Swift.Sendable {
44+
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 = ""
51+
}
52+
}
53+
"""
54+
.write(to: outFile, atomically: true, encoding: .utf8)
55+
56+
print("Successfully parsed '\(dir)' and wrote to '\(outFile)'")
57+
}
58+
59+
private enum Error: Swift.Error {
60+
case swiftExtensionNotInOutfile
61+
}
62+
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+
)
86+
}
87+
}
88+
}
89+
90+
private enum FileOrDir {
91+
case dir(canonical: String, [Self])
92+
case file(canonical: String, ext: String)
93+
94+
func code(_ indent: Int = 2, path: [String] = []) -> String {
95+
switch self {
96+
case let .dir(canonical, items):
97+
"""
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+
"""
106+
case let .file(canonical, ext):
107+
"""
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))}
115+
"""
116+
}
117+
}
118+
}
119+
}
120+
121+
private extension String {
122+
func pascalCase() -> Self {
123+
self.split { !$0.isLetter && !$0.isNumber }
124+
.map {
125+
if let first = $0.first?.uppercased() {
126+
return first[...] + $0.dropFirst()
127+
} else {
128+
return $0
129+
}
130+
}
131+
.joined()
132+
}
133+
134+
func camelCase() -> Self {
135+
var initialLowercased = false
136+
return self.split { !$0.isLetter && !$0.isNumber }
137+
.map {
138+
if !initialLowercased {
139+
defer { initialLowercased = true }
140+
if let first = $0.first?.lowercased() {
141+
return first[...] + $0.dropFirst()
142+
} else {
143+
return $0.lowercased()[...]
144+
}
145+
} else if let first = $0.first?.uppercased() {
146+
return first[...] + $0.dropFirst()
147+
} else {
148+
return $0
149+
}
150+
}
151+
.joined()
152+
}
153+
}

Sources/Pages/Models/Post+AllCases.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import Foundation
2+
import Dependencies
23

34
extension Post: CaseIterable {
45
static var allCases: [Self] {
5-
[
6+
@Dependency(\.publicAssets) var assets
7+
8+
return [
69
Self(
710
id: 1,
811
title: "PrismUI \u{2014} Controlling MSI RGB Keyboard on mac",
@@ -14,7 +17,7 @@ extension Post: CaseIterable {
1417
),
1518
Self(
1619
id: 2,
17-
header: .image("/assets/projects/anime-now/an-discover.png", label: "Anime Now! discover image"),
20+
header: .image(assets.projects.animeNow.anDiscover.path, label: "Anime Now! discover image"),
1821
title: "Anime Now! \u{2014} An iOS and macOS App",
1922
content: """
2023
> TBD

Sources/Pages/Models/Post.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct Post {
2222
}
2323

2424
var slug: String {
25-
"\(id)-\(self.title.slug())"
25+
"\(id)-\(self.title.split { !$0.isLetter && !$0.isNumber }.joined(separator: "-").lowercased())"
2626
}
2727

2828
enum Header {
@@ -31,6 +31,10 @@ struct Post {
3131
case video(String)
3232
case code(String, lang: CodeLang)
3333

34+
func test() {
35+
let assets = PublicAssets()
36+
}
37+
3438
enum CodeLang: String {
3539
case swift
3640
case rust
@@ -68,10 +72,4 @@ struct Post {
6872
}
6973
}
7074
}
71-
}
72-
73-
private extension String {
74-
func slug() -> String {
75-
split { !$0.isLetter && !$0.isNumber }.joined(separator: "-").lowercased()
76-
}
7775
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Dependencies
2+
3+
private enum PublicAssetsKey: DependencyKey {
4+
static let liveValue = PublicAssets("/assets")
5+
}
6+
7+
public extension DependencyValues {
8+
var publicAssets: PublicAssets {
9+
get { self[PublicAssetsKey.self] }
10+
set { self[PublicAssetsKey.self] = newValue }
11+
}
12+
}

0 commit comments

Comments
 (0)