Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.serena/
16 changes: 15 additions & 1 deletion Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ playbook_configuration:
| Key | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `target` | Target name used for snapshot generation. Default: *FirstTarget* |
| `test_target_path` | Path to unit test directory. Snapshots will be written to its `__Snapshots__` folder. Default: target name folder |
| `test_target_path` | Path to unit test directory. Snapshots will be written to its `__Snapshots__` folder. Supports template parameters (see below). Default: target name folder |
| `test_file_path` | Output file path for generated tests. Default: DerivedData or resolved via plugin |
| `template_file_path` | Custom template path relative to target. Optional. Defaults:‣ *PreviewTests.stencil* for test plugin‣ *PreviewModels.stencil* for playbook plugin|
| `simulator_device` | Device identifier used to run tests (e.g. `iPhone15,2`). Optional |
Expand All @@ -56,6 +56,20 @@ playbook_configuration:

---

### 🎯 Template Parameters

The `test_target_path` configuration supports template placeholders `${TARGET}` and `${TEST_TARGET}` that are dynamically replaced at runtime. Useful for multi-target projects and monorepos.

**Example:**
```yaml
test_configuration:
target: MyApp
test_target_path: "${PROJECT_DIR}/${TARGET}/Tests"
# Resolves to: ${PROJECT_DIR}/MyApp/Tests
```

---

📌 You can define both `test_configuration` and `playbook_configuration` at once.

Prefire will use these settings when generating files either via:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,46 @@ private enum Constants {
}

struct GeneratedPlaybookOptions {
var targetPath: String?
var sources: [Path]
var output: Path
var previewDefaultEnabled: Bool
var template: Path?
var cacheBasePath: Path?
var imports: [String]?
var testableImports: [String]?

init(targetPath: String?, sources: [String], output: String?, template: String?, cacheBasePath: String?, config: Config?) throws {
self.targetPath = config?.playbook.targetPath ?? targetPath
self.sources = sources.isEmpty ? [.current] : sources.compactMap({ Path($0) })
let targetPath: String?
let sources: [Path]
let output: Path
let previewDefaultEnabled: Bool
let template: Path?
let cacheBasePath: Path?
let imports: [String]?
let testableImports: [String]?
}

self.output = (output.flatMap({ Path($0) }) ?? .current) + Constants.defaultOutputName
// MARK: - Factory

previewDefaultEnabled = config?.playbook.previewDefaultEnabled ?? true
extension GeneratedPlaybookOptions {
/// Creates GeneratedPlaybookOptions by merging CLI options with Config
/// - Parameters:
/// - cli: Raw CLI options
/// - config: Loaded config (optional)
/// - Returns: Fully resolved GeneratedPlaybookOptions
static func from(
cli: CLIPlaybookOptions,
config: Config?
) -> GeneratedPlaybookOptions {
let targetPath = config?.playbook.targetPath ?? cli.targetPath

if let template = config?.playbook.template, let targetPath {
let targetURL = URL(filePath: targetPath)
let templateURL = targetURL.appending(path: template)
self.template = Path(templateURL.absoluteURL.path(percentEncoded: false))
} else if let template {
self.template = Path(template)
}
let template = OptionsResolver.resolveTemplate(
cliTemplate: cli.template,
configTemplate: config?.playbook.template,
targetPath: targetPath.flatMap { Path($0) }
)

self.cacheBasePath = cacheBasePath.flatMap({ Path($0) })
imports = config?.playbook.imports
testableImports = config?.playbook.testableImports
return GeneratedPlaybookOptions(
targetPath: targetPath,
sources: OptionsResolver.resolveSources(cli.sources),
output: OptionsResolver.resolveOutput(cli.output, defaultFileName: Constants.defaultOutputName),
previewDefaultEnabled: config?.playbook.previewDefaultEnabled ?? true,
template: template,
cacheBasePath: cli.cacheBasePath.flatMap { Path($0) },
imports: config?.playbook.imports,
testableImports: config?.playbook.testableImports
)
}
}

Expand Down
31 changes: 20 additions & 11 deletions PrefireExecutable/Sources/prefire/Commands/Playbook/Playbook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,27 @@ extension Prefire {

func run() async throws {
Logger.level = verbose ? .verbose : .warnings
let config = Config.load(from: config, testTargetPath: nil, env: ProcessInfo.processInfo.environment)

try await GeneratePlaybookCommand.run(
GeneratedPlaybookOptions(
targetPath: targetPath,
sources: sources,
output: output,
template: template,
cacheBasePath: cacheBasePath,
config: config
)

let cliOptions = CLIPlaybookOptions(
targetPath: targetPath,
template: template,
sources: sources,
output: output,
cacheBasePath: cacheBasePath
)

let loadedConfig = Config.load(
from: config,
testTargetPath: nil,
env: ProcessInfo.processInfo.environment
)

let options = GeneratedPlaybookOptions.from(
cli: cliOptions,
config: loadedConfig
)

try await GeneratePlaybookCommand.run(options)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,55 +8,67 @@ private enum Constants {
}

struct GeneratedTestsOptions {
var target: String?
var testTarget: String?
var template: Path?
var sources: [Path]
var output: Path
var prefireEnabledMarker: Bool
var testTargetPath: Path?
var cacheBasePath: Path?
var device: String?
var osVersion: String?
var snapshotDevices: [String]?
var imports: [String]?
var testableImports: [String]?
var useGroupedSnapshots: Bool
let target: String?
let testTarget: String?
let template: Path?
let sources: [Path]
let output: Path
let prefireEnabledMarker: Bool
let testTargetPath: Path?
let cacheBasePath: Path?
let device: String?
let osVersion: String?
let snapshotDevices: [String]?
let imports: [String]?
let testableImports: [String]?
let useGroupedSnapshots: Bool
}

// MARK: - Factory

extension GeneratedTestsOptions {
/// Creates GeneratedTestsOptions by merging CLI options with Config
/// - Parameters:
/// - cli: Raw CLI options
/// - config: Loaded config (optional)
/// - Returns: Fully resolved GeneratedTestsOptions
static func from(cli: CLITestsOptions, config: Config?) -> GeneratedTestsOptions {
let target = config?.tests.target ?? cli.target
let testTarget = cli.testTarget
let sources = config?.tests.sources ?? cli.sources
let output = config?.tests.testFilePath ?? cli.output
let device = config?.tests.device ?? cli.device
let osVersion = config?.tests.osVersion ?? cli.osVersion

init(
target: String?,
testTarget: String?,
template: String?,
sources: [String],
output: String?,
testTargetPath: String?,
cacheBasePath: String?,
device: String?,
osVersion: String?,
config: Config?
) throws {
self.target = config?.tests.target ?? target
self.testTarget = testTarget
self.testTargetPath = (config?.tests.testTargetPath ?? testTargetPath).flatMap({ Path($0) })
let rawTestTargetPath = config?.tests.testTargetPath ?? cli.testTargetPath
let testTargetPath = OptionsResolver.resolvePath(
rawTestTargetPath,
target: target,
testTarget: testTarget
)

if let template = config?.tests.template, let testTargetPath = self.testTargetPath {
let testTargetURL = URL(filePath: testTargetPath.string)
let templateURL = testTargetURL.appending(path: template)
self.template = Path(templateURL.absoluteURL.path(percentEncoded: false))
} else if let template {
self.template = Path(template)
}
let template = OptionsResolver.resolveTemplate(
cliTemplate: cli.template,
configTemplate: config?.tests.template,
targetPath: testTargetPath
)

self.sources = (config?.tests.sources ?? sources).compactMap({ Path($0) })
self.output = (config?.tests.testFilePath ?? output).flatMap({ Path($0) }) ?? .current
prefireEnabledMarker = config?.tests.previewDefaultEnabled ?? true
self.cacheBasePath = cacheBasePath.flatMap({ Path($0) })
self.device = config?.tests.device ?? device
self.osVersion = config?.tests.osVersion ?? osVersion
useGroupedSnapshots = config?.tests.useGroupedSnapshots ?? true
snapshotDevices = config?.tests.snapshotDevices
imports = config?.tests.imports
testableImports = config?.tests.testableImports
return GeneratedTestsOptions(
target: target,
testTarget: testTarget,
template: template,
sources: OptionsResolver.resolveSources(sources),
output: output.flatMap { Path($0) } ?? .current,
prefireEnabledMarker: config?.tests.previewDefaultEnabled ?? true,
testTargetPath: testTargetPath,
cacheBasePath: cli.cacheBasePath.flatMap { Path($0) },
device: device,
osVersion: osVersion,
snapshotDevices: config?.tests.snapshotDevices,
imports: config?.tests.imports,
testableImports: config?.tests.testableImports,
useGroupedSnapshots: config?.tests.useGroupedSnapshots ?? true
)
}
}

Expand Down
39 changes: 24 additions & 15 deletions PrefireExecutable/Sources/prefire/Commands/Tests/Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,31 @@ extension Prefire {

func run() async throws {
Logger.level = verbose ? .verbose : .warnings
let config = Config.load(from: config, testTargetPath: testTargetPath, env: ProcessInfo.processInfo.environment)

try await GenerateTestsCommand.run(
GeneratedTestsOptions(
target: target,
testTarget: testTarget,
template: template,
sources: sources,
output: output,
testTargetPath: testTargetPath,
cacheBasePath: cacheBasePath,
device: device,
osVersion: osVersion,
config: config
)

let cliOptions = CLITestsOptions(
target: target,
testTarget: testTarget,
template: template,
sources: sources,
output: output,
testTargetPath: testTargetPath,
cacheBasePath: cacheBasePath,
device: device,
osVersion: osVersion
)

let loadedConfig = Config.load(
from: config,
testTargetPath: testTargetPath,
env: ProcessInfo.processInfo.environment
)

let options = GeneratedTestsOptions.from(
cli: cliOptions,
config: loadedConfig
)

try await GenerateTestsCommand.run(options)
}
}
}
40 changes: 40 additions & 0 deletions PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation

/// Utility for resolving template placeholders in configuration paths
///
/// Supports the following placeholders:
/// - `${TARGET}` - Replaced with the target name
/// - `${TEST_TARGET}` - Replaced with the test target name
///
/// Example:
/// ```swift
/// let path = "${PROJECT_DIR}/${TARGET}/Tests"
/// let resolved = ConfigPathResolver.resolve(path, target: "MyApp")
/// // Result: "${PROJECT_DIR}/MyApp/Tests"
/// ```
enum ConfigPathResolver {
/// Resolves template placeholders in a configuration path
///
/// - Parameters:
/// - path: The path string containing placeholders
/// - target: Optional target name to replace ${TARGET}
/// - testTarget: Optional test target name to replace ${TEST_TARGET}
/// - Returns: Resolved path with placeholders replaced by their values
static func resolve(
_ path: String,
target: String? = nil,
testTarget: String? = nil
) -> String {
var result = path

if let target {
result = result.replacingOccurrences(of: "${TARGET}", with: target)
}

if let testTarget {
result = result.replacingOccurrences(of: "${TEST_TARGET}", with: testTarget)
}

return result
}
}
10 changes: 10 additions & 0 deletions PrefireExecutable/Sources/prefire/Options/CLIPlaybookOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

/// Raw CLI options for Playbook command - no merging or resolution logic
struct CLIPlaybookOptions {
let targetPath: String?
let template: String?
let sources: [String]
let output: String?
let cacheBasePath: String?
}
14 changes: 14 additions & 0 deletions PrefireExecutable/Sources/prefire/Options/CLITestsOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

/// Raw CLI options for Tests command - no merging or resolution logic
struct CLITestsOptions {
let target: String?
let testTarget: String?
let template: String?
let sources: [String]
let output: String?
let testTargetPath: String?
let cacheBasePath: String?
let device: String?
let osVersion: String?
}
Loading