Skip to content

Instantly share code, notes, and snippets.

@danthorpe
Last active March 26, 2023 16:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danthorpe/ae765df338e8a0ed5fcbdd6add9c7b12 to your computer and use it in GitHub Desktop.
Save danthorpe/ae765df338e8a0ed5fcbdd6add9c7b12 to your computer and use it in GitHub Desktop.
Swift Package Manager DSL
/// ✂️ Copy everything below this into other Package.swift files
/// to create the DSL capabilities described at
/// https://dan.works/hyper-modularization/
/// ------------------------------------------------------------
// MARK: - 🪄 Package Helpers
extension String {
var dependency: Target.Dependency {
Target.Dependency.target(name: self)
}
var snapshotTests: String { "\(self)SnapshotTests" }
var tests: String { "\(self)Tests" }
}
struct Module {
enum ProductType {
case library(Product.Library.LibraryType? = nil)
}
typealias Builder = (inout Self) -> Void
static func builder(withDefaults defaults: Module) -> (Builder?) -> Module {
{ block in
var module = Self(
name: "TO BE REPLACED",
defaultWith: defaults.defaultWith,
swiftSettings: defaults.swiftSettings,
plugins: defaults.plugins
)
block?(&module)
return module.merged(with: defaults)
}
}
var name: String
var group: String?
var dependsOn: [String]
let defaultWith: [Target.Dependency]
var with: [Target.Dependency]
var createProduct: ProductType?
var createTarget: Bool
var createUnitTests: Bool
var unitTestsDependsOn: [String]
var unitTestsWith: [Target.Dependency]
var createSnapshotTests: Bool
var snapshotTestsDependsOn: [String]
var resources: [Resource]?
var swiftSettings: [SwiftSetting]
var plugins: [Target.PluginUsage]
var dependencies: [Target.Dependency] {
defaultWith + with + dependsOn.map { $0.dependency }
}
var productTargets: [String] {
createTarget ? [name] : dependsOn
}
init(
name: String,
group: String? = nil,
dependsOn: [String] = [],
defaultWith: [Target.Dependency] = [],
with: [Target.Dependency] = [],
createProduct: ProductType? = nil,
createTarget: Bool = true,
createUnitTests: Bool = true,
unitTestsDependsOn: [String] = [],
unitTestsWith: [Target.Dependency] = [],
createSnapshotTests: Bool = false,
snapshotTestsDependsOn: [String] = [],
resources: [Resource]? = nil,
swiftSettings: [SwiftSetting] = [],
plugins: [Target.PluginUsage] = []
) {
self.name = name
self.group = group
self.dependsOn = dependsOn
self.defaultWith = defaultWith
self.with = with
self.createProduct = createProduct
self.createTarget = createTarget
self.createUnitTests = createUnitTests
self.unitTestsDependsOn = unitTestsDependsOn
self.unitTestsWith = unitTestsWith
self.createSnapshotTests = createSnapshotTests
self.snapshotTestsDependsOn = snapshotTestsDependsOn
self.resources = resources
self.swiftSettings = swiftSettings
self.plugins = plugins
}
private func merged(with other: Self) -> Self {
var copy = self
copy.dependsOn = Set(dependsOn).union(other.dependsOn).sorted()
copy.unitTestsDependsOn = Set(unitTestsDependsOn).union(other.unitTestsDependsOn).sorted()
copy.snapshotTestsDependsOn = Set(snapshotTestsDependsOn).union(other.snapshotTestsDependsOn).sorted()
return copy
}
func group(by newGroup: String) -> Self {
var copy = self
if let group {
copy.group = "\(newGroup)/\(group)"
} else {
copy.group = newGroup
}
return copy
}
}
extension Package {
func add(module: Module) {
// Check should create a product
if case let .library(type) = module.createProduct {
products.append(
.library(
name: module.name,
type: type,
targets: module.productTargets
)
)
}
// Check should create a target
if module.createTarget {
let path = "\(module.group ?? "")/Sources/\(module.name)"
targets.append(
.target(
name: module.name,
dependencies: module.dependencies,
path: path,
resources: module.resources,
swiftSettings: module.swiftSettings,
plugins: module.plugins
)
)
}
// Check should add unit tests
if module.createUnitTests {
let path = "\(module.group ?? "")/Tests/\(module.name.tests)"
targets.append(
.testTarget(
name: module.name.tests,
dependencies: [module.name.dependency] + module.unitTestsDependsOn.map { $0.dependency } +
module.unitTestsWith + [.customDump],
path: path
)
)
}
// Check should add snapshot tests
if module.createSnapshotTests {
let path = "\(module.group ?? "")/Tests/\(module.name.snapshotTests)"
targets.append(
.testTarget(
name: module.name.snapshotTests,
dependencies: [module.name.dependency] +
module.snapshotTestsDependsOn.map { $0.dependency } + [
.customDump
],
path: path
)
)
}
}
}
protocol ModuleGroupConvertible {
func makeGroup() -> [Module]
}
@resultBuilder
struct GroupBuilder {
static func buildBlock() -> [Module] { [] }
static func buildBlock(_ modules: ModuleGroupConvertible...) -> [Module] {
modules.flatMap { $0.makeGroup() }
}
}
extension Module: ModuleGroupConvertible {
func makeGroup() -> [Module] { [self] }
}
struct ModuleGroup: ModuleGroupConvertible {
var name: String
var modules: [Module]
init(_ name: String, @GroupBuilder builder: () -> [Module]) {
self.name = name
self.modules = builder()
}
func makeGroup() -> [Module] {
modules.map { $0.group(by: name) }
}
}
infix operator <>
extension String {
/// Adds the string as a module to the package, allowing for inline
/// customization with no defaults. Handy for base modules like Utilities
/// which are then used inside the builders/defaults.
static func <> (lhs: String, rhs: Module.Builder) -> Module {
var module = Module(name: lhs)
rhs(&module)
return module
}
}
infix operator <+
extension String {
/// Creates a new Module using the lhs as the name
static func <+ (lhs: String, rhs: Module) -> Module {
var module = rhs
module.name = lhs
return module
}
}
infix operator <<&
extension String {
/// Nest modules into groups
static func <<& (groupName: String, @ModuleBuilder builder: () -> [Module]) -> ModuleGroup {
ModuleGroup(groupName, builder: builder)
}
}
infix operator <&
extension String {
/// Add groups of modules together under a logical directory
static func <& (groupName: String, @ModuleBuilder builder: () -> [Module]) {
let modules = ModuleGroup(groupName, builder: builder).makeGroup()
for module in modules {
package.add(module: module)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment