diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/FileManager+Utils.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/FileManager+Utils.swift index 3a7f3ed0..56f8ae85 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/FileManager+Utils.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/FileManager+Utils.swift @@ -2,7 +2,7 @@ import Foundation extension FileManager { - func moveFiles(in directory: String, to destination: String) { + func moveFiles(in directory: String, to destination: String) throws { let currentDirectory = currentDirectoryPath let files = try? contentsOfDirectory( atPath: "\(currentDirectory)/\(directory)" @@ -10,64 +10,44 @@ extension FileManager { if let files = files { for file in files { guard file != ".DS_Store" else { continue } - do { - try moveItem( - atPath: "\(currentDirectory)/\(directory)/\(file)", - toPath:"\(currentDirectory)/\(destination)/\(file)" - ) - } catch { - print("Error \(error)") - } + try moveItem( + atPath: "\(currentDirectory)/\(directory)/\(file)", + toPath:"\(currentDirectory)/\(destination)/\(file)" + ) } } } - func rename(file: String, to destination: String) { + func rename(file: String, to destination: String) throws { let currentDirectory = currentDirectoryPath - do { - try moveItem( - atPath: "\(currentDirectory)/\(file)", - toPath:"\(currentDirectory)/\(destination)" - ) - } catch { - print("Error \(error)") - } + try moveItem( + atPath: "\(currentDirectory)/\(file)", + toPath:"\(currentDirectory)/\(destination)" + ) } - func copy(file: String, to destination: String) { + func copy(file: String, to destination: String) throws { let currentDirectory = currentDirectoryPath - do { - try copyItem( - atPath: "\(currentDirectory)/\(file)", - toPath:"\(currentDirectory)/\(destination)" - ) - } catch { - print("Error \(error)") - } + try copyItem( + atPath: "\(currentDirectory)/\(file)", + toPath:"\(currentDirectory)/\(destination)" + ) } - func createDirectory(path: String) { + func createDirectory(path: String) throws { let currentDirectory = currentDirectoryPath - do { - try createDirectory(atPath: "\(currentDirectory)/\(path)", withIntermediateDirectories: true, attributes: nil) - } catch { - print("Error \(error)") - } + try createDirectory(atPath: "\(currentDirectory)/\(path)", withIntermediateDirectories: true, attributes: nil) } - func createFile(name: String, at directory: String) { + func createFile(name: String, at directory: String) throws { let currentDirectory = currentDirectoryPath - createDirectory(path: directory) + try createDirectory(path: directory) createFile(atPath: "\(currentDirectory)\(directory)\(name)", contents: nil) } - func removeItems(in directory: String) { + func removeItems(in directory: String) throws { let currentDirectory = currentDirectoryPath - do { - try removeItem(atPath: "\(currentDirectory)/\(directory)") - } catch { - print("Error \(error)") - } + try removeItem(atPath: "\(currentDirectory)/\(directory)") } func replaceAllOccurrences(of original: String, to replacing: String) { diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Confirm.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Confirm.swift new file mode 100644 index 00000000..f9e6c31b --- /dev/null +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Confirm.swift @@ -0,0 +1,20 @@ +// +// Confirm.swift +// +// +// Created by MarkG on 17/7/24. +// + +import Foundation + +enum ConfirmResult: String, Titlable, CaseIterable { + + case yes = "Yes" + case no = "No" + + var title: String { rawValue } +} + +func confirm(_ message: String) -> ConfirmResult { + picker(title: message, options: ConfirmResult.allCases) +} diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Picker.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Picker.swift index 873a4d78..42242ddb 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Picker.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Picker.swift @@ -48,18 +48,23 @@ func picker(title: String, options: [T]) -> T { cursorOff() write("◆".foreColor(81).bold) moveRight() - write(title) + writeln(title) - let currentLine = readCursorPos().row + 1 + options.forEach { + writeln($0.title) + } + moveUp(options.count) + clearBelow() + let currentLine = readCursorPos().row let state = OptionState( options: options.enumerated() .map { Option(title: $1.title, line: currentLine + $0) }, activeLine: currentLine, rangeOfLines: (currentLine, currentLine + options.count - 1) ) - reRender(state: state) + let restoreLine = options.count + 1 while true { clearBuffer() @@ -76,12 +81,14 @@ func picker(title: String, options: [T]) -> T { if state.activeLine > state.rangeOfLines.minimum { state.activeLine -= 1 + moveUp(restoreLine) reRender(state: state) } case .down: if state.activeLine < state.rangeOfLines.maximum { state.activeLine += 1 + moveUp(restoreLine) reRender(state: state) } default: break @@ -96,21 +103,20 @@ private func reRender(state: OptionState) { (state.rangeOfLines.minimum...state.rangeOfLines.maximum).forEach { line in let isActive = line == state.activeLine - moveLineDown() - writeAt(line, 1, "│".foreColor(81)) + write("│".foreColor(81)) moveRight() let stateIndicator = isActive ? "●".lightGreen : "○".foreColor(250) - writeAt(line, 3, stateIndicator) + write(stateIndicator) if let title = state.options.first(where: { $0.line == line })?.title { let title = isActive ? title : title.foreColor(250) - writeAt(line, 5, title) + moveRight() + writeln(title) } } - moveLineDown() writeln("└".foreColor(81)) } diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Terminal.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Terminal.swift index d2c15177..ba565658 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Terminal.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Terminal.swift @@ -39,13 +39,13 @@ func writeAt(_ row: Int, _ col: Int, _ text: String) { func ask( _ q: String, note: String? = nil, - isRequired: Bool = true, + defaultValue: String? = nil, onValidate: (_ input: String) -> String? ) -> String { write("◆".foreColor(81).bold) moveRight() - if isRequired { + if defaultValue == nil { write(q) moveRight() writeln("(*)".red) @@ -56,7 +56,6 @@ func ask( if let note { writeln(note.gray) } - let currentLine = readCursorPos().row var hasError = false while(true) { @@ -65,7 +64,9 @@ func ask( if let error = onValidate(input) { hasError = true write(error, style: .error) - moveTo(currentLine, 1) + + let count = getWroteLineCount(error) + 1 + moveUp(count) clearLine() continue } @@ -73,36 +74,54 @@ func ask( if hasError { clearBelow() } - return input - } -} -func step(title: String, action: () throws -> Void) { - let currentLine = readCursorPos().row - let writeTitle: (_ isSuccess: Bool?) -> Void = { - if let isSuccess = $0 { - write(isSuccess ? "✔".green : "⛌".red) - } else { - write("✔".gray) + if input.isEmpty, + let defaultValue { + return defaultValue } - moveRight() - writeln(title.bold) + return input } - writeTitle(nil) - moveDown() +} + +func step(title: String, action: () throws -> Void) throws { + writeln("→ \(title.uppercased())".bold) do { try action() - moveTo(currentLine, 0) - writeTitle(true) } catch { let message = error as? String ?? error.localizedDescription write(message, style: .error) + throw error + } +} + +private func getWroteLineCount(_ text: String) -> Int { + let count = unexpand(text: text).count + let col = readScreenSize().col + return Int(ceil(Double(count) / Double(col))) +} - let savedLine = readCursorPos().row - moveTo(currentLine, 0) - writeTitle(false) - moveTo(savedLine, 0) +private func unexpand(text: String) -> String { + var result = "" + var spaceCount = 0 + for char in text { + if char == " " { + spaceCount += 1 + } else { + if spaceCount > 0 { + let tabs = String(repeating: "\t", count: spaceCount / 8) + let spaces = String(repeating: " ", count: spaceCount % 8) + result += tabs + spaces + spaceCount = 0 + } + result += String(char) + } + } + if spaceCount > 0 { + let tabs = String(repeating: "\t", count: spaceCount / 8) + let spaces = String(repeating: " ", count: spaceCount % 8) + result += tabs + spaces } + return result } diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpCICDService.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpCICDService.swift index 774b9f09..9f212855 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpCICDService.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpCICDService.swift @@ -2,7 +2,7 @@ import Foundation struct SetUpCICDService { - enum CICDService { + enum CICDService: Titlable, CaseIterable { case github, bitrise, codemagic, later @@ -25,9 +25,22 @@ struct SetUpCICDService { return nil } } + + var title: String { + switch self { + case .github: + "Github" + case .bitrise: + "Bitrise" + case .codemagic: + "CodeMagic" + case .later: + "none" + } + } } - enum GithubRunnerType { + enum GithubRunnerType: Titlable, CaseIterable { case macOSLatest, selfHosted, later @@ -48,59 +61,62 @@ struct SetUpCICDService { return nil } } + + var title: String { + switch self { + case .macOSLatest: + "macos" + case .selfHosted: + "self-hosted" + case .later: + "none" + } + } } private let fileManager = FileManager.default - func perform() { - var service: CICDService? = nil - while service == nil { - print("Which CI/CD service do you use (Can be edited later) [(g)ithub/(b)itrise/(c)odemagic/(l)ater]: ") - service = CICDService(readLine().string) - } + func perform() throws { + let service = picker( + title: "Which service do you use?", + options: CICDService.allCases + ) switch service { case .github: - var runnerType: GithubRunnerType? - while runnerType == nil { - print("Which workflow runner do you want to use? [(m)acos-latest/(s)elf-hosted/(l)ater]: ") - runnerType = GithubRunnerType(readLine().string) - } - print("Setting template for Github Actions") - fileManager.removeItems(in: "bitrise.yml") - fileManager.removeItems(in: "codemagic.yaml") - fileManager.removeItems(in: ".github/workflows") - fileManager.createDirectory(path: ".github/workflows") + let runnerType = picker( + title: "Which workflow runner do you want to use?", + options: GithubRunnerType.allCases + ) + + try fileManager.removeItems(in: "bitrise.yml") + try fileManager.removeItems(in: "codemagic.yaml") + try fileManager.removeItems(in: ".github/workflows") + try fileManager.createDirectory(path: ".github/workflows") switch runnerType { case .macOSLatest: - print("Configured to run on the latest macOS.") - fileManager.moveFiles(in: ".github/project_workflows", to: ".github/workflows") - fileManager.removeItems(in: ".github/project_workflows") - fileManager.removeItems(in: ".github/self_hosted_project_workflows") + try fileManager.moveFiles(in: ".github/project_workflows", to: ".github/workflows") + try fileManager.removeItems(in: ".github/project_workflows") + try fileManager.removeItems(in: ".github/self_hosted_project_workflows") case .selfHosted: - print("Configured to run on self-hosted.") - fileManager.moveFiles(in: ".github/self_hosted_project_workflows", to: ".github/workflows") - fileManager.removeItems(in: ".github/project_workflows") - fileManager.removeItems(in: ".github/self_hosted_project_workflows") - case .later, .none: + try fileManager.moveFiles(in: ".github/self_hosted_project_workflows", to: ".github/workflows") + try fileManager.removeItems(in: ".github/project_workflows") + try fileManager.removeItems(in: ".github/self_hosted_project_workflows") + case .later: print("You can manually setup the runner later.") } case .bitrise: - print("Setting template for Bitrise") - fileManager.removeItems(in: "codemagic.yaml") - fileManager.removeItems(in: ".github/workflows") - fileManager.removeItems(in: ".github/project_workflows") - fileManager.removeItems(in: ".github/self_hosted_project_workflows") + try fileManager.removeItems(in: "codemagic.yaml") + try fileManager.removeItems(in: ".github/workflows") + try fileManager.removeItems(in: ".github/project_workflows") + try fileManager.removeItems(in: ".github/self_hosted_project_workflows") case .codemagic: - print("Setting template for CodeMagic") - fileManager.removeItems(in: "bitrise.yml") - fileManager.removeItems(in: ".github/workflows") - fileManager.removeItems(in: ".github/project_workflows") - fileManager.removeItems(in: ".github/self_hosted_project_workflows") - case .later, .none: + try fileManager.removeItems(in: "bitrise.yml") + try fileManager.removeItems(in: ".github/workflows") + try fileManager.removeItems(in: ".github/project_workflows") + try fileManager.removeItems(in: ".github/self_hosted_project_workflows") + case .later: print("You can manually setup the template later.") } - - print("✅ Completed") } } diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpDeliveryConstants.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpDeliveryConstants.swift index e33e8608..6d1e2c61 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpDeliveryConstants.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpDeliveryConstants.swift @@ -1,21 +1,11 @@ struct SetUpDeliveryConstants { - func perform() { - print("Do you want to set up Constants values? (Can be edited later) [Y/n]: ") - - let arg = readLine() ?? "y" - - switch arg.lowercased() { - case "y", "yes": - do { - let error = try safeShell("open -a Xcode fastlane/Constants/Constant.swift") - guard let error = error, !error.isEmpty else { break } - print("Could not open Xcode. Make sure Xcode is installed and try again.\nRaw error: \(error)") - } catch { - print("Error: \(error)") - } - default: - print("✅ Completed. You can edit this file at 'fastlane/Constants/Constant.swift'.") + func perform() throws { + let result = confirm("Do you want to set up Constants values?") + switch result { + case .yes: + try safeShell("open -a Xcode fastlane/Constants/Constant.swift") + case .no: break } } } diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpInterface.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpInterface.swift index a769577e..5f61dcf5 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpInterface.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpInterface.swift @@ -29,11 +29,11 @@ struct SetUpInterface { private let fileManager = FileManager.default - func perform(_ interface: Interface, _ projectName: String) { + func perform(_ interface: Interface, _ projectName: String) throws { switch interface { case .swiftUI: let swiftUIAppDirectory = "tuist/Interfaces/SwiftUI/Sources/Application" - fileManager.rename( + try fileManager.rename( file: "\(swiftUIAppDirectory)/App.swift", to: "\(swiftUIAppDirectory)/\(projectName)App.swift" ) @@ -42,8 +42,8 @@ struct SetUpInterface { let folderName = interface.folderName - fileManager.moveFiles(in: "tuist/Interfaces/\(folderName)/Project", to: "") - fileManager.moveFiles(in: "tuist/Interfaces/\(folderName)/Sources", to: "\(projectName)/Sources") - fileManager.removeItems(in: "tuist/Interfaces") + try fileManager.moveFiles(in: "tuist/Interfaces/\(folderName)/Project", to: "") + try fileManager.moveFiles(in: "tuist/Interfaces/\(folderName)/Sources", to: "\(projectName)/Sources") + try fileManager.removeItems(in: "tuist/Interfaces") } } diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpiOSProject.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpiOSProject.swift index 517fdda4..bfdee35d 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpiOSProject.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpiOSProject.swift @@ -32,60 +32,58 @@ class SetUpIOSProject { } func perform() { - step(title: "Fill project information") { - readArguments() - } - - step(title: "Replace files structure") { - replaceFileStructure() - } + do { + try step(title: "Fill project information") { + readArguments() + } - step(title: "Create placeholder files") { - createPlaceholderFiles() - } + try step(title: "Replace files structure") { + try replaceFileStructure() + } - step(title: "Setup interface") { - SetUpInterface().perform(interface ?? .uiKit, projectName) - } + try step(title: "Create placeholder files") { + try createPlaceholderFiles() + } - step(title: "Replace package and package name within files") { - try replaceTextInFiles() - } + try step(title: "Setup interface") { + try SetUpInterface().perform(interface ?? .uiKit, projectName) + } - step(title: "Run tuist") { - try runTuist() - } + try step(title: "Replace package and package name within files") { + try replaceTextInFiles() + } - step(title: "Install dependencies") { - try installDependencies() - } + try step(title: "Run tuist") { + try runTuist() + } - step(title: "Remove gitkeep files from project") { - try removeGitkeepFromXcodeProject() - } + try step(title: "Install dependencies") { + try installDependencies() + } - step(title: "Remove template files") { - try removeTemplateFiles() - } + try step(title: "Remove gitkeep files from project") { + try removeGitkeepFromXcodeProject() + } - step(title: "Setup CI/CD") { - setUpCICD() - } + try step(title: "Remove template files") { + try removeTemplateFiles() + } - moveDown() - _ = ask("Please press Esc to exit...") + try step(title: "Setup CI/CD") { + try setUpCICD() + } -// -// -// print("=> 🚀 Done! App is ready to be tested 🙌") -// try? openProject() + writeln() + write("🚀 Done! App is ready to development 🙌", style: .success) + try? openProject() + } catch {} } private func readArguments() { var canMoveDown = false let tryMoveDown: () -> Void = { if canMoveDown { - moveDown() + writeln() } canMoveDown = true @@ -124,14 +122,14 @@ class SetUpIOSProject { if minimumVersion.isEmpty { tryMoveDown() - let version = ask( + + let defaultVersion = "14.0" + minimumVersion = ask( "Which is the iOS minimum version?", - note: "Ex: 14.0", - isRequired: false, + note: "Default: \(defaultVersion)", + defaultValue: defaultVersion, onValidate: validateVersion ) - - minimumVersion = !version.isEmpty ? version : "14.0" } moveDown() @@ -141,21 +139,21 @@ class SetUpIOSProject { ) } - private func replaceFileStructure() { - fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)Tests", to: "\(projectNameNoSpace)Tests") - fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)KIFUITests", to: "\(projectNameNoSpace)KIFUITests") - fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)", to: "\(projectNameNoSpace)") + private func replaceFileStructure() throws { + try fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)Tests", to: "\(projectNameNoSpace)Tests") + try fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)KIFUITests", to: "\(projectNameNoSpace)KIFUITests") + try fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)", to: "\(projectNameNoSpace)") } - private func createPlaceholderFiles() { + private func createPlaceholderFiles() throws { // Duplicate the env example file to env file - fileManager.copy(file: ".env.example", to: ".env") + try fileManager.copy(file: ".env.example", to: ".env") // Add AutoMockable.generated.swift file - fileManager.createFile(name: "AutoMockable.generated.swift", at: "\(projectNameNoSpace)Tests/Sources/Mocks/Sourcery") + try fileManager.createFile(name: "AutoMockable.generated.swift", at: "\(projectNameNoSpace)Tests/Sources/Mocks/Sourcery") // Add R.generated.swift file. - fileManager.createFile(name: "R.generated.swift", at: "\(projectNameNoSpace)/Sources/Supports/Helpers/Rswift") + try fileManager.createFile(name: "R.generated.swift", at: "\(projectNameNoSpace)/Sources/Supports/Helpers/Rswift") } private func replaceTextInFiles() throws { @@ -197,28 +195,27 @@ class SetUpIOSProject { } private func removeTemplateFiles() throws { - fileManager.removeItems(in: ".tuist-version") - fileManager.removeItems(in: "tuist") - fileManager.removeItems(in: "Project.swift") - fileManager.removeItems(in: "Workspace.swift") - - fileManager.removeItems(in: ".github/workflows/test_uikit_install_script.yml") - fileManager.removeItems(in: ".github/workflows/test_swiftui_install_script.yml") - fileManager.removeItems(in: ".git/index") + try fileManager.removeItems(in: ".tuist-version") + try fileManager.removeItems(in: "tuist") + try fileManager.removeItems(in: "Project.swift") + try fileManager.removeItems(in: "Workspace.swift") + + try fileManager.removeItems(in: ".github/workflows/test_uikit_install_script.yml") + try fileManager.removeItems(in: ".github/workflows/test_swiftui_install_script.yml") + try fileManager.removeItems(in: ".git/index") try safeShell("git reset") } - private func setUpCICD() { + private func setUpCICD() throws { if !isCI { - SetUpCICDService().perform() - SetUpDeliveryConstants().perform() - fileManager.removeItems(in: "Scripts") + try SetUpCICDService().perform() + try SetUpDeliveryConstants().perform() + try fileManager.removeItems(in: "Scripts") } } private func openProject() throws { if !isCI { - print("=> 🛠 Opening the project.") try safeShell("open -a Xcode \(projectNameNoSpace).xcworkspace") } } @@ -236,6 +233,10 @@ class SetUpIOSProject { private func validateVersion(_ version: String) -> String? { + if version.isEmpty { + return nil + } + let versionRegex="^[0-9_]+(\\.[0-9]+)+$" let valid = version ~= versionRegex