Skip to content

Commit 73fc441

Browse files
committed
🧰 Adds plugin for streamlining creating new TCA features.
1 parent 0718720 commit 73fc441

File tree

2 files changed

+247
-0
lines changed

2 files changed

+247
-0
lines changed

Package.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ let package = Package(
1515
.library(name: "Extensions", targets: ["Extensions"]),
1616
.library(name: "HTTPNetworking", targets: ["HTTPNetworking"]),
1717
.library(name: "Identified", targets: ["Identified"]),
18+
.plugin(name: "Create TCA Feature", targets: ["Create TCA Feature"])
1819
],
1920
targets: [
2021
.target(name: "Cache"),
@@ -28,5 +29,18 @@ let package = Package(
2829

2930
.target(name: "Identified"),
3031
.testTarget(name: "IdentifiedTests", dependencies: ["Identified"]),
32+
33+
.plugin(
34+
name: "Create TCA Feature",
35+
capability: .command(
36+
intent: .custom(
37+
verb: "create-tca-feature",
38+
description: "Generates the source files for a new TCA feauture."
39+
),
40+
permissions: [
41+
.writeToPackageDirectory(reason: "Generates source code."),
42+
]
43+
)
44+
)
3145
]
3246
)
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import Foundation
2+
import PackagePlugin
3+
4+
@main
5+
struct CreateTCAFeature: CommandPlugin {
6+
7+
// MARK: Error
8+
9+
struct Error: Swift.Error {
10+
let description: String
11+
}
12+
13+
// MARK: CommandPlugin
14+
15+
func performCommand(context: PluginContext, arguments: [String]) async throws {
16+
var extractor = ArgumentExtractor(arguments)
17+
18+
let features = extractor.extractOption(named: "create")
19+
guard !features.isEmpty else {
20+
throw Error(description: "Must provide a feature to create using --create <feature>")
21+
}
22+
23+
for feature in features {
24+
let featureDirectory = context.package.directory.appending(["Sources", feature])
25+
try createTCAFeatureDirectory(featureDirectory)
26+
try createTCAFeatureFile(for: feature, directory: featureDirectory)
27+
try createTCAFeatureActionFile(for: feature, directory: featureDirectory)
28+
try createTCAFeatureStateFile(for: feature, directory: featureDirectory)
29+
try createTCAFeaturePathFile(for: feature, directory: featureDirectory)
30+
try createTCAFeatureViewFile(for: feature, directory: featureDirectory)
31+
32+
}
33+
}
34+
}
35+
36+
// MARK: - Create TCA Feature Folder
37+
38+
extension CreateTCAFeature {
39+
func createTCAFeatureDirectory(_ directory: Path) throws {
40+
try FileManager.default.createDirectory(at: URL(filePath: directory.string), withIntermediateDirectories: true)
41+
}
42+
}
43+
44+
// MARK: - Create TCA Feature
45+
46+
extension CreateTCAFeature {
47+
func createTCAFeatureFile(for feature: String, directory: Path) throws {
48+
let content = """
49+
import SwiftUI
50+
import ComposableArchitecture
51+
52+
public struct \(feature): Reducer {
53+
54+
// MARK: Initializers
55+
56+
public init() {}
57+
58+
// MARK: Body
59+
60+
public var body: some ReducerOf<Self> {
61+
BindingReducer()
62+
63+
Reduce<State, Action> { state, action in
64+
switch action {
65+
case .binding:
66+
return .none
67+
case .delegate:
68+
return .none
69+
}
70+
}
71+
}
72+
}
73+
"""
74+
75+
return try content.write(
76+
to: URL(filePath: directory.appending(subpath: "\(feature).swift").string),
77+
atomically: false,
78+
encoding: .utf8
79+
)
80+
}
81+
}
82+
83+
// MARK: - Create TCA Feature Action
84+
85+
extension CreateTCAFeature {
86+
func createTCAFeatureActionFile(for feature: String, directory: Path) throws {
87+
let content = """
88+
import SwiftUI
89+
import ComposableArchitecture
90+
91+
extension \(feature) {
92+
public enum Action: Equatable, BindableAction {
93+
case binding(BindingAction<State>)
94+
case delegate(Delegate)
95+
96+
// MARK: Delegate
97+
98+
public enum Delegate: Equatable {
99+
100+
}
101+
}
102+
}
103+
"""
104+
105+
return try content.write(
106+
to: URL(filePath: directory.appending(subpath: "\(feature)+Action.swift").string),
107+
atomically: false,
108+
encoding: .utf8
109+
)
110+
}
111+
}
112+
113+
// MARK: - Create TCA Feature State
114+
115+
extension CreateTCAFeature {
116+
func createTCAFeatureStateFile(for feature: String, directory: Path) throws {
117+
let content = """
118+
import SwiftUI
119+
import ComposableArchitecture
120+
121+
extension \(feature) {
122+
public struct State: Equatable {
123+
124+
// MARK: Properties
125+
126+
// MARK: Initializers
127+
128+
public init() {
129+
130+
}
131+
}
132+
}
133+
"""
134+
135+
return try content.write(
136+
to: URL(filePath: directory.appending(subpath: "\(feature)+State.swift").string),
137+
atomically: false,
138+
encoding: .utf8
139+
)
140+
}
141+
}
142+
143+
// MARK: - Create TCA Feature Path
144+
145+
extension CreateTCAFeature {
146+
func createTCAFeaturePathFile(for feature: String, directory: Path) throws {
147+
let content = """
148+
import Foundation
149+
import ComposableArchitecture
150+
151+
extension \(feature) {
152+
public struct Path: Reducer {
153+
154+
// MARK: State
155+
156+
public enum State: Equatable {
157+
case child
158+
}
159+
160+
// MARK: Action
161+
162+
public enum Action: Equatable {
163+
case child
164+
}
165+
166+
// MARK: Body
167+
168+
public var body: some ReducerOf<Self> {
169+
Scope(state: /State.child, action: /Action.child) {
170+
ChildFeature()
171+
}
172+
}
173+
}
174+
}
175+
"""
176+
177+
return try content.write(
178+
to: URL(filePath: directory.appending(subpath: "\(feature)+Path.swift").string),
179+
atomically: false,
180+
encoding: .utf8
181+
)
182+
}
183+
}
184+
185+
// MARK: - Create TCA Feature View
186+
187+
extension CreateTCAFeature {
188+
func createTCAFeatureViewFile(for feature: String, directory: Path) throws {
189+
let name = feature.replacingOccurrences(of: "Feature", with: "")
190+
let content = """
191+
import SwiftUI
192+
import ComposableArchitecture
193+
194+
// MARK: - \(name)View
195+
196+
public struct \(name)View: View {
197+
198+
// MARK: Properties
199+
200+
let store: StoreOf<\(feature)>
201+
202+
// MARK: Initializers
203+
204+
public init(store: StoreOf<\(feature)>) {
205+
self.store = store
206+
}
207+
208+
// MARK: Body
209+
210+
public var body: some View {
211+
WithViewStore(store, observe: { $0 }) { viewStore in
212+
213+
}
214+
}
215+
}
216+
217+
// MARK: - \(name)View + Previews
218+
219+
#Preview {
220+
\(name)View(store: .init(
221+
initialState: \(feature).State(),
222+
reducer: { \(feature)() }
223+
))
224+
}
225+
"""
226+
227+
return try content.write(
228+
to: URL(filePath: directory.appending(subpath: "\(name)View.swift").string),
229+
atomically: true,
230+
encoding: .utf8
231+
)
232+
}
233+
}

0 commit comments

Comments
 (0)