diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..0e42e03 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,26 @@ +name: Build and Test + +on: + pull_request: + branches: + - main + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + name: Run Tests + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: swift-actions/setup-swift@v2 + with: + swift-version: "6.1.0" + - name: Build + run: swift build + - name: Tests + run: swift test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0455eee..13f76c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: swift-actions/setup-swift@v2 with: - swift-version: "5.9" + swift-version: "6.1.0" - name: Build for Release run: swift build -c release --arch arm64 --arch x86_64 --product cap2spm - name: Zip Executable diff --git a/Package.resolved b/Package.resolved index 9643ee6..6ca936d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "a9e7f2e877dd7be707ed96f55ced97ddc6979140faf5cbe72c86c7ca76ddef73", "pins" : [ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version" : "1.2.3" + "revision" : "011f0c765fb46d9cac61bca19be0527e99c98c8b", + "version" : "1.5.1" } }, { @@ -14,10 +15,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", - "version" : "509.0.0" + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index e240df4..7f6c995 100644 --- a/Package.swift +++ b/Package.swift @@ -1,33 +1,61 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "capacitor-plugin-converter", - platforms: [.macOS(.v13)], + platforms: [.macOS(.v15)], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.1"), - .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), + .package(url: "https://github.com/apple/swift-syntax.git", from: "601.0.1"), ], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. .executableTarget( name: "cap2spm", dependencies: [ + .target(name: "CapacitorPluginSyntaxTools"), + .target(name: "JavascriptPackageTools"), + .target(name: "CapacitorPluginTools"), .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftParser", package: "swift-syntax") - ] + ], + path: "Sources/CommandLineTool" ), - .testTarget(name: "CapacitorConverterTests", + .target(name: "CapacitorPluginSyntaxTools", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax") + ] + ), + .target(name: "CapacitorPluginTools", + dependencies: [ + .target(name: "CapacitorPluginSyntaxTools"), + .target(name: "JavascriptPackageTools") + ] + ), + .target(name: "JavascriptPackageTools"), + + // Test Targets + .testTarget(name: "JavascriptPackageToolsTests", dependencies: [ - .target(name: "cap2spm"), - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "SwiftSyntax", package: "swift-syntax"), - .product(name: "SwiftParser", package: "swift-syntax") - ] - ) + .target(name: "JavascriptPackageTools"), + ], + resources: [.copy("package-test.json")] + ), + .testTarget(name: "CapacitorPluginSyntaxToolsTests", + dependencies: [ + .target(name: "CapacitorPluginSyntaxTools"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax") + ] + ), + .testTarget(name: "CapacitorPluginToolsTests", + dependencies: [ + .target(name: "CapacitorPluginTools") + ] + ), ] ) diff --git a/Sources/NewPluginGeneration/AddPluginToClass.swift b/Sources/CapacitorPluginSyntaxTools/AddPluginToClass.swift similarity index 100% rename from Sources/NewPluginGeneration/AddPluginToClass.swift rename to Sources/CapacitorPluginSyntaxTools/AddPluginToClass.swift diff --git a/Sources/CapacitorPluginSyntaxTools/CapacitorPlugin.swift b/Sources/CapacitorPluginSyntaxTools/CapacitorPlugin.swift new file mode 100644 index 0000000..0a63519 --- /dev/null +++ b/Sources/CapacitorPluginSyntaxTools/CapacitorPlugin.swift @@ -0,0 +1,27 @@ +import Foundation +import SwiftSyntax +import SwiftParser + +public struct CapacitorPlugin { + public let identifier: String + public let jsName: String + public var methods: [CapacitorPluginMethod] = [] + + public init(identifier: String, jsName: String) { + self.identifier = identifier + self.jsName = jsName + } + + public func modifySwiftFile(at fileURL: URL) throws { + let source = try String(contentsOf: fileURL, encoding: .utf8) + let sourceFile = Parser.parse(source: source) + + let capSyntax = CapacitorPluginSyntax(plugin: self) + + let incremented = AddPluginToClass(with: capSyntax).visit(sourceFile) + + var outputString: String = "" + incremented.write(to: &outputString) + try outputString.write(to: fileURL, atomically: true, encoding: .utf8) + } +} diff --git a/Sources/CapacitorPluginSyntaxTools/CapacitorPluginMethod.swift b/Sources/CapacitorPluginSyntaxTools/CapacitorPluginMethod.swift new file mode 100644 index 0000000..4109f80 --- /dev/null +++ b/Sources/CapacitorPluginSyntaxTools/CapacitorPluginMethod.swift @@ -0,0 +1,17 @@ +import Foundation +import SwiftSyntax +import SwiftParser + +public struct CapacitorPluginMethod { + public let methodName: String + public let returnType: CapacitorPluginReturnType + + var syntax: CapacitorPluginMethodSyntax { + CapacitorPluginMethodSyntax(methodName: methodName, returnType: returnType) + } + + public init(methodName: String, returnType: CapacitorPluginReturnType) { + self.methodName = methodName + self.returnType = returnType + } +} diff --git a/Sources/NewPluginGeneration/CapacitorPluginMethod.swift b/Sources/CapacitorPluginSyntaxTools/CapacitorPluginMethodSyntax.swift similarity index 98% rename from Sources/NewPluginGeneration/CapacitorPluginMethod.swift rename to Sources/CapacitorPluginSyntaxTools/CapacitorPluginMethodSyntax.swift index 36af2e7..63e0c78 100644 --- a/Sources/NewPluginGeneration/CapacitorPluginMethod.swift +++ b/Sources/CapacitorPluginSyntaxTools/CapacitorPluginMethodSyntax.swift @@ -2,10 +2,10 @@ import Foundation import SwiftSyntax import SwiftParser -struct CapacitorPluginMethod { +struct CapacitorPluginMethodSyntax { let methodName: String let returnType: CapacitorPluginReturnType - + private let nameIdent = TokenSyntax.identifier("name") private let returnTypeIdent = TokenSyntax.identifier("returnType") private let declRef = DeclReferenceExprSyntax(baseName: .identifier("CAPPluginMethod")) diff --git a/Sources/NewPluginGeneration/CapacitorPluginReturnType.swift b/Sources/CapacitorPluginSyntaxTools/CapacitorPluginReturnType.swift similarity index 85% rename from Sources/NewPluginGeneration/CapacitorPluginReturnType.swift rename to Sources/CapacitorPluginSyntaxTools/CapacitorPluginReturnType.swift index d3840e8..b5681aa 100644 --- a/Sources/NewPluginGeneration/CapacitorPluginReturnType.swift +++ b/Sources/CapacitorPluginSyntaxTools/CapacitorPluginReturnType.swift @@ -2,12 +2,12 @@ import Foundation import SwiftSyntax import SwiftParser -enum CapacitorPluginReturnType: String { +public enum CapacitorPluginReturnType: String { case none case promise case callback - init?(with typeString: String) { + public init?(with typeString: String) { switch typeString { case "CAPPluginReturnNone": self = .none diff --git a/Sources/NewPluginGeneration/CapacitorPluginSyntax.swift b/Sources/CapacitorPluginSyntaxTools/CapacitorPluginSyntax.swift similarity index 97% rename from Sources/NewPluginGeneration/CapacitorPluginSyntax.swift rename to Sources/CapacitorPluginSyntaxTools/CapacitorPluginSyntax.swift index 9ca0290..388136e 100644 --- a/Sources/NewPluginGeneration/CapacitorPluginSyntax.swift +++ b/Sources/CapacitorPluginSyntaxTools/CapacitorPluginSyntax.swift @@ -3,16 +3,14 @@ import SwiftSyntax import SwiftParser struct CapacitorPluginSyntax { - let identifier: String - let jsName: String - var methods: [CapacitorPluginMethod] = [] - + let plugin: CapacitorPlugin + let defaultIndent = 4 - + func createMemberBlock() -> MemberBlockItemListSyntax { - let pluginIdentifier = addPublicStringConstantVariable(variableName: "identifier", stringValue: identifier) + let pluginIdentifier = addPublicStringConstantVariable(variableName: "identifier", stringValue: plugin.identifier) - let jsName = addPublicStringConstantVariable(variableName: "jsName", stringValue: jsName) + let jsName = addPublicStringConstantVariable(variableName: "jsName", stringValue: plugin.jsName) let pluginMethods = addPluginMethodDeclarations() @@ -88,9 +86,9 @@ struct CapacitorPluginSyntax { var functionArray: [ArrayElementSyntax] = [] - for method in methods { + for method in plugin.methods { let arrayElement = ArrayElementSyntax(leadingTrivia: .spaces(defaultIndent*2), - expression: method.functionCallExpr, + expression: method.syntax.functionCallExpr, trailingComma: .commaToken(trailingTrivia: .newline)) functionArray.append(arrayElement) diff --git a/Sources/CapacitorPluginPackage.swift b/Sources/CapacitorPluginTools/CapacitorPluginPackage.swift similarity index 75% rename from Sources/CapacitorPluginPackage.swift rename to Sources/CapacitorPluginTools/CapacitorPluginPackage.swift index 2cadad4..0d93fee 100644 --- a/Sources/CapacitorPluginPackage.swift +++ b/Sources/CapacitorPluginTools/CapacitorPluginPackage.swift @@ -1,11 +1,6 @@ -// -// File.swift -// -// -// Created by Mark Anderson on 10/16/23. -// - import Foundation +import CapacitorPluginSyntaxTools +import JavascriptPackageTools enum CapacitorPluginError: Error { case objcFileCount(Int) @@ -24,18 +19,22 @@ enum CapacitorPluginError: Error { } } -class CapacitorPluginPackage { - let pluginDirectoryName: String - let basePathURL: URL - let packageJSONURL: URL - let pluginSrcDirectoryURL: URL - let iosSrcDirectoryURL: URL - let files: [URL] +public class CapacitorPluginPackage { + public let pluginDirectoryName: String + public let basePathURL: URL + public let packageJSONURL: URL + public let pluginSrcDirectoryURL: URL + public let iosSrcDirectoryURL: URL + public let files: [URL] - var oldPlugin: OldPlugin? + private var oldPlugin: OldPlugin? private var packageJSONParser: PackageJSONParser + + public var plugin: CapacitorPlugin? { + oldPlugin?.capacitorPlugin + } - init(directoryName: String) throws { + public init(directoryName: String) throws { pluginDirectoryName = directoryName let fileManager = FileManager.default @@ -53,7 +52,7 @@ class CapacitorPluginPackage { files = try fileManager.contentsOfDirectory(at: pluginSrcDirectoryURL, includingPropertiesForKeys: nil) } - func findObjCPluginFile() throws -> URL { + public func findObjCPluginFile() throws -> URL { let mfiles = files.filter { $0.absoluteString.hasSuffix(".m") } guard mfiles.count == 1, let url = mfiles.first else { throw CapacitorPluginError.objcFileCount(mfiles.count) } @@ -63,18 +62,18 @@ class CapacitorPluginPackage { return url } - func parseObjCPluginFile(at url: URL) throws { + public func parseObjCPluginFile(at url: URL) throws { oldPlugin = try OldPlugin(at: url) } - func findObjCHeaderFile() throws -> URL { + public func findObjCHeaderFile() throws -> URL { let headerFiles = files.filter { $0.absoluteString.hasSuffix(".h") } guard headerFiles.count == 1, let url = headerFiles.first else { throw CapacitorPluginError.objcFileCount(headerFiles.count) } return url } - func findSwiftPluginFile() throws -> URL { + public func findSwiftPluginFile() throws -> URL { guard let oldPlugin else { throw CapacitorPluginError.oldPluginMissing } let fileName = "\(oldPlugin.capacitorPlugin.identifier).swift" @@ -82,7 +81,7 @@ class CapacitorPluginPackage { return URL(filePath: fileName, directoryHint: .notDirectory, relativeTo: pluginSrcDirectoryURL) } - func findPodspecFile() throws -> URL { + public func findPodspecFile() throws -> URL { let fileName = packageJSONParser.podspec return URL(filePath: fileName, directoryHint: .notDirectory, relativeTo: basePathURL) diff --git a/Sources/OldPluginParsing/OldPlugin.swift b/Sources/CapacitorPluginTools/OldPlugin.swift similarity index 91% rename from Sources/OldPluginParsing/OldPlugin.swift rename to Sources/CapacitorPluginTools/OldPlugin.swift index 3d190e9..7fa892d 100644 --- a/Sources/OldPluginParsing/OldPlugin.swift +++ b/Sources/CapacitorPluginTools/OldPlugin.swift @@ -1,13 +1,14 @@ import Foundation import RegexBuilder +import CapacitorPluginSyntaxTools enum OldPluginParserError: Error { case pluginMissing case podspecNameMissing } -class OldPlugin { - var capacitorPlugin: CapacitorPluginSyntax +package class OldPlugin { + var capacitorPlugin: CapacitorPlugin init(at fileURL: URL) throws { let pluginSourceText = try String(contentsOf: fileURL, encoding: .utf8) @@ -15,7 +16,7 @@ class OldPlugin { try matchMethods(text: pluginSourceText) } - private static func matchPlugin(text: String) throws -> CapacitorPluginSyntax { + private static func matchPlugin(text: String) throws -> CapacitorPlugin { let identiferRef = Reference(Substring.self) let jsNameRef = Reference(Substring.self) @@ -38,7 +39,7 @@ class OldPlugin { } if let match = text.firstMatch(of: pluginNameRegex) { - return CapacitorPluginSyntax(identifier: String(match[identiferRef]), jsName: String(match[jsNameRef])) + return CapacitorPlugin(identifier: String(match[identiferRef]), jsName: String(match[jsNameRef])) } else { throw OldPluginParserError.pluginMissing } diff --git a/Sources/PackageSwiftGenerator/GeneratePackageFile.swift b/Sources/CapacitorPluginTools/PackageFileGenerator.swift similarity index 78% rename from Sources/PackageSwiftGenerator/GeneratePackageFile.swift rename to Sources/CapacitorPluginTools/PackageFileGenerator.swift index a73ddb2..61a0bd6 100644 --- a/Sources/PackageSwiftGenerator/GeneratePackageFile.swift +++ b/Sources/CapacitorPluginTools/PackageFileGenerator.swift @@ -1,6 +1,6 @@ import Foundation -class GeneratePackageFile { +public class PackageFileGenerator { let packageName: String let libName: String let capRepoName = "capacitor-swift-pm" @@ -40,8 +40,15 @@ class GeneratePackageFile { """ } - init(packageName: String, libName: String) { + public init(packageName: String, libName: String) { self.packageName = packageName self.libName = libName } + + public func generateFile(at fileURL: URL) throws { + let packageFileURL = URL(filePath: "Package.swift", directoryHint: .notDirectory, relativeTo: fileURL.baseURL) + let packageFileString = packageText + + try packageFileString.write(to: packageFileURL, atomically: true, encoding: .utf8) + } } diff --git a/Sources/OldPluginParsing/PodspecParser.swift b/Sources/CapacitorPluginTools/PodspecParser.swift similarity index 88% rename from Sources/OldPluginParsing/PodspecParser.swift rename to Sources/CapacitorPluginTools/PodspecParser.swift index 97653a3..da16725 100644 --- a/Sources/OldPluginParsing/PodspecParser.swift +++ b/Sources/CapacitorPluginTools/PodspecParser.swift @@ -1,10 +1,10 @@ import Foundation import RegexBuilder -struct PodspecParser { - let podName: String +public struct PodspecParser { + public let podName: String - init(at fileURL: URL) throws { + public init(at fileURL: URL) throws { let pluginSourceText = try String(contentsOf: fileURL, encoding: .utf8) podName = try PodspecParser.podnameMatcher(text: pluginSourceText) } diff --git a/Sources/CommandLineTool/CLIArguments.swift b/Sources/CommandLineTool/CLIArguments.swift new file mode 100644 index 0000000..a838942 --- /dev/null +++ b/Sources/CommandLineTool/CLIArguments.swift @@ -0,0 +1,35 @@ +import Foundation +import CapacitorPluginTools + +extension Cap2SPM { + func getUrlsForArgs(package: CapacitorPluginPackage, + objcHeader: String?, + objcFile: String?, + swiftFile: String?) throws -> (URL, URL, URL) { + + let mFileURL: URL + let swiftFileURL: URL + let hFileURL: URL + + if let objcHeader { + hFileURL = URL(filePath: objcHeader, directoryHint: .notDirectory) + } else { + hFileURL = try package.findObjCHeaderFile() + } + + if let objcFile { + mFileURL = URL(filePath: objcFile, directoryHint: .notDirectory) + try package.parseObjCPluginFile(at: mFileURL) + } else { + mFileURL = try package.findObjCPluginFile() + } + + if let swiftFile { + swiftFileURL = URL(filePath: swiftFile, directoryHint: .notDirectory) + } else { + swiftFileURL = try package.findSwiftPluginFile() + } + + return (mFileURL, swiftFileURL, hFileURL) + } +} diff --git a/Sources/CommandLineTool/CLIFileOperations.swift b/Sources/CommandLineTool/CLIFileOperations.swift new file mode 100644 index 0000000..1ed163d --- /dev/null +++ b/Sources/CommandLineTool/CLIFileOperations.swift @@ -0,0 +1,83 @@ +import Foundation +import CapacitorPluginTools + +extension Cap2SPM { + func deleteFiles(at fileList: [URL], shouldBackup: Bool) throws { + if shouldBackup { + try fileBackup(of: fileList) + } else { + try fileDelete(of: fileList) + } + } + + func moveSourceDirectories(for package: CapacitorPluginPackage) throws { + guard let identifer = package.plugin?.identifier else { return } + + try moveItemCreatingIntermediaryDirectories(at: package.iosSrcDirectoryURL.appending(path: "Plugin"), + to: package.iosSrcDirectoryURL.appending(path: "Sources").appending(path: identifer)) + + try moveItemCreatingIntermediaryDirectories(at: package.iosSrcDirectoryURL.appending(path: "PluginTests"), + to: package.iosSrcDirectoryURL.appending(path: "Tests").appending(path: "\(identifer)Tests")) + } + + func moveItemCreatingIntermediaryDirectories(at: URL, to: URL) throws { + print("Moving \(at.path()) to \(to.path())...") + let parentPath = to.deletingLastPathComponent() + if !FileManager.default.fileExists(atPath: parentPath.path) { + try FileManager.default.createDirectory(at: parentPath, withIntermediateDirectories: true, attributes: nil) + } + try FileManager.default.moveItem(at: at, to: to) + } + + func fileBackup(of fileURLs: [URL]) throws { + for fileURL in fileURLs { + let fileBackupURL = fileURL.appendingPathExtension("old") + print("Moving \(fileURL.path()) to \(fileBackupURL.path())...") + try FileManager.default.moveItem(at: fileURL, to: fileBackupURL) + } + } + + func fileDelete(of fileURLs: [URL]) throws { + for fileURL in fileURLs { + print("Deleting \(fileURL.path())...") + try FileManager.default.removeItem(at: fileURL) + } + } + + func modifyGitignores(for package: CapacitorPluginPackage) throws { + let baseEntries = ["/Packages", + "xcuserdata/", + "DerivedData/", + ".swiftpm/configuration/registries.json", + ".swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata", + ".netrc"] + + let rootEntries = ["Pods", + "Podfile.lock", + "Package.resolved", + "Build", + "xcuserdata", + "/.build"] + + let iosEntries = [".DS_Store", ".build"] + + let rootGitignore = package.basePathURL.appending(path: ".gitignore") + let iOSGitignore = package.iosSrcDirectoryURL.appending(path: ".gitignore") + + try modifyGitignore(at: rootGitignore, with: baseEntries + rootEntries) + try modifyGitignore(at: iOSGitignore, with: baseEntries + iosEntries) + } + + func modifyGitignore(at fileURL: URL, with content: [String]) throws { + var gitignoreText = "" + if FileManager.default.fileExists(atPath: fileURL.path()) { + gitignoreText = try String(contentsOf: fileURL, encoding: .utf8) + } + content.forEach { + if !gitignoreText.contains($0) { + gitignoreText.append("\n\($0)") + } + } + try gitignoreText.write(to: fileURL, atomically: true, encoding: .utf8) + } +} diff --git a/Sources/CommandLineTool/cap2spm.swift b/Sources/CommandLineTool/cap2spm.swift new file mode 100644 index 0000000..3b3e028 --- /dev/null +++ b/Sources/CommandLineTool/cap2spm.swift @@ -0,0 +1,62 @@ +import Foundation +import ArgumentParser +import CapacitorPluginTools +import JavascriptPackageTools +import CapacitorPluginSyntaxTools + +@main +struct Cap2SPM: ParsableCommand { + @Flag(name: .customLong("backup"), inversion: .prefixedNo, help: "Should we make a backup?") + var shouldBackup = false + + @Option(help: "Objective-C header for file containing CAP_PLUGIN macro") + var objcHeader: String? + + @Option(help: "Objective-C file containing CAP_PLUGIN macro") + var objcFile: String? + + @Option(help: "Swift file containing class inheriting from CAPPlugin") + var swiftFile: String? + + @Argument(help: "Plugin Directory") + var pluginDirectory: String + + mutating func run() throws { + let mFileURL: URL + let swiftFileURL: URL + let hFileURL: URL + + let capacitorPluginPackage = try CapacitorPluginPackage(directoryName: pluginDirectory) + + (mFileURL, swiftFileURL, hFileURL) = try getUrlsForArgs(package: capacitorPluginPackage, + objcHeader: objcHeader, + objcFile: objcFile, + swiftFile: swiftFile) + + + let podspecFileURL = try capacitorPluginPackage.findPodspecFile() + let podspec = try PodspecParser(at: podspecFileURL) + + guard let capPlugin = capacitorPluginPackage.plugin else { return } + + try capPlugin.modifySwiftFile(at: swiftFileURL) + + let packageGenerator = PackageFileGenerator(packageName: podspec.podName, libName: capPlugin.identifier) + + try packageGenerator.generateFile(at: podspecFileURL) + + var unneededFiles = [hFileURL, mFileURL] + let oldFiles = ["Plugin/Info.plist", + "PluginTests/Info.plist", + "Plugin.xcodeproj", + "Plugin.xcworkspace", + "Podfile"].compactMap { + capacitorPluginPackage.iosSrcDirectoryURL.appending(path: $0) + } + unneededFiles.append(contentsOf: oldFiles) + + try deleteFiles(at: unneededFiles, shouldBackup: shouldBackup) + + try modifyGitignores(for: capacitorPluginPackage) + } +} diff --git a/Sources/OldPluginParsing/PackageJSON.swift b/Sources/JavascriptPackageTools/PackageJSON.swift similarity index 87% rename from Sources/OldPluginParsing/PackageJSON.swift rename to Sources/JavascriptPackageTools/PackageJSON.swift index d0f6d5a..2173d3b 100644 --- a/Sources/OldPluginParsing/PackageJSON.swift +++ b/Sources/JavascriptPackageTools/PackageJSON.swift @@ -5,7 +5,8 @@ struct PackageJSON: Codable { let version: String let files: [String] let capacitor: Capacitor - + let scripts: [String: String] + struct Capacitor: Codable { let ios: Ios struct Ios: Codable { diff --git a/Sources/OldPluginParsing/PackageJSONParser.swift b/Sources/JavascriptPackageTools/PackageJSONParser.swift similarity index 70% rename from Sources/OldPluginParsing/PackageJSONParser.swift rename to Sources/JavascriptPackageTools/PackageJSONParser.swift index 0d44b13..8c17cc1 100644 --- a/Sources/OldPluginParsing/PackageJSONParser.swift +++ b/Sources/JavascriptPackageTools/PackageJSONParser.swift @@ -4,24 +4,24 @@ enum PackageJSONError: Error { case noPodspec } -struct PackageJSONParser: CustomDebugStringConvertible { +public struct PackageJSONParser: CustomDebugStringConvertible { private let package: PackageJSON - var npmName: String { + public var npmName: String { package.name } - var version: String { + public var version: String { package.version } - var podspec: String = "" + public var podspec: String = "" - var iosSrcDirectory: String { + public var iosSrcDirectory: String { package.capacitor.ios.src } - var pluginDirectories: [String] { + public var pluginDirectories: [String] { var plugins: [String] = [] for file in package.files { @@ -32,23 +32,18 @@ struct PackageJSONParser: CustomDebugStringConvertible { return plugins } + + public var scripts: [String: String] { + package.scripts + } - init(with url: URL) throws { + public init(with url: URL) throws { let data = try Data(contentsOf: url) package = try JSONDecoder().decode(PackageJSON.self, from: data) podspec = try findPodspec() } - - private func findPodspec() throws -> String { - for file in package.files { - if file.hasSuffix("podspec") { - return file - } - } - throw PackageJSONError.noPodspec - } - - var debugDescription: String { + + public var debugDescription: String { """ NPM Name: \(npmName) Version: \(version) @@ -57,4 +52,13 @@ struct PackageJSONParser: CustomDebugStringConvertible { Plugin Directories: \(pluginDirectories) """ } + + private func findPodspec() throws -> String { + for file in package.files { + if file.hasSuffix("podspec") { + return file + } + } + throw PackageJSONError.noPodspec + } } diff --git a/Sources/cap2spm.swift b/Sources/cap2spm.swift deleted file mode 100644 index 3f658ad..0000000 --- a/Sources/cap2spm.swift +++ /dev/null @@ -1,153 +0,0 @@ -import Foundation -import SwiftParser -import SwiftSyntax -import ArgumentParser - -@main -struct Cap2SPM: ParsableCommand { - @Flag(name: .customLong("backup"), inversion: .prefixedNo, help: "Should we make a backup?") - var shouldBackup = false - - @Option(help: "Objective-C header for file containing CAP_PLUGIN macro") - var objcHeader: String? - - @Option(help: "Objective-C file containing CAP_PLUGIN macro") - var objcFile: String? - - @Option(help: "Swift file containing class inheriting from CAPPlugin") - var swiftFile: String? - - @Argument(help: "Plugin Directory") - var pluginDirectory: String - - mutating func run() throws { - let mFileURL: URL - let swiftFileURL: URL - let hFileURL: URL - - let capacitorPluginPackage = try CapacitorPluginPackage(directoryName: pluginDirectory) - - if let objcHeader { - hFileURL = URL(filePath: objcHeader, directoryHint: .notDirectory) - } else { - hFileURL = try capacitorPluginPackage.findObjCHeaderFile() - } - - if let objcFile { - mFileURL = URL(filePath: objcFile, directoryHint: .notDirectory) - try capacitorPluginPackage.parseObjCPluginFile(at: mFileURL) - } else { - mFileURL = try capacitorPluginPackage.findObjCPluginFile() - } - - if let swiftFile { - swiftFileURL = URL(filePath: swiftFile, directoryHint: .notDirectory) - } else { - swiftFileURL = try capacitorPluginPackage.findSwiftPluginFile() - } - - let podspecFileURL = try capacitorPluginPackage.findPodspecFile() - - guard let capPlugin = capacitorPluginPackage.oldPlugin?.capacitorPlugin else { return } - - try modifySwiftFile(at: swiftFileURL, plugin: capPlugin) - try generatePackageSwiftFile(at: podspecFileURL, plugin: capPlugin) - try modifyPodspec(at: podspecFileURL) - try modifyGitignores(with: capacitorPluginPackage) - - var fileList = [mFileURL, hFileURL] - if shouldBackup { - try fileBackup(of: fileList) - } else { - let oldFiles = ["Plugin/Info.plist", "PluginTests/Info.plist", "Plugin.xcodeproj", "Plugin.xcworkspace", "Podfile"].compactMap { - capacitorPluginPackage.iosSrcDirectoryURL.appending(path: $0) - } - fileList.append(contentsOf: oldFiles) - try fileDelete(of: fileList) - } - try moveItemCreatingIntermediaryDirectories(at: capacitorPluginPackage.iosSrcDirectoryURL.appending(path: "Plugin"), to: capacitorPluginPackage.iosSrcDirectoryURL.appending(path: "Sources").appending(path: capPlugin.identifier)) - try moveItemCreatingIntermediaryDirectories(at: capacitorPluginPackage.iosSrcDirectoryURL.appending(path: "PluginTests"), to: capacitorPluginPackage.iosSrcDirectoryURL.appending(path: "Tests").appending(path: "\(capPlugin.identifier)Tests")) - } - - private func fileBackup(of fileURLs: [URL]) throws { - for fileURL in fileURLs { - let fileBackupURL = fileURL.appendingPathExtension("old") - print("Moving \(fileURL.path()) to \(fileBackupURL.path())...") - try FileManager.default.moveItem(at: fileURL, to: fileBackupURL) - } - } - - private func fileDelete(of fileURLs: [URL]) throws { - for fileURL in fileURLs { - print("Deleting \(fileURL.path())...") - try FileManager.default.removeItem(at: fileURL) - } - } - - private func modifySwiftFile(at fileURL: URL, plugin: CapacitorPluginSyntax) throws { - let source = try String(contentsOf: fileURL, encoding: .utf8) - let sourceFile = Parser.parse(source: source) - - let incremented = AddPluginToClass(with: plugin).visit(sourceFile) - - if shouldBackup { - try fileBackup(of: [fileURL]) - } else { - try fileDelete(of: [fileURL]) - } - - var outputString: String = "" - incremented.write(to: &outputString) - try outputString.write(to: fileURL, atomically: true, encoding: .utf8) - } - - private func generatePackageSwiftFile(at fileURL: URL, plugin: CapacitorPluginSyntax) throws { - let podspec = try PodspecParser(at: fileURL) - - let packageFileURL = URL(filePath: "Package.swift", directoryHint: .notDirectory, relativeTo: fileURL.baseURL) - let packageFile = GeneratePackageFile(packageName: podspec.podName, libName: plugin.identifier) - let packageFileString = packageFile.packageText - - try packageFileString.write(to: packageFileURL, atomically: true, encoding: .utf8) - } - - private func modifyPodspec(at fileURL: URL) throws { - var podspecText = try String(contentsOf: fileURL, encoding: .utf8) - podspecText = podspecText.replacingOccurrences(of: "/Plugin/", with: "/Sources/") - try podspecText.write(to: fileURL, atomically: true, encoding: .utf8) - } - - private func modifyGitignores(with package: CapacitorPluginPackage) throws { - let baseEntries = ["/Packages", "xcuserdata/" ,"DerivedData/", ".swiftpm/configuration/registries.json", ".swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata", ".netrc"] - var rootEntries = ["Pods", "Podfile.lock", "Package.resolved", "Build", "xcuserdata", "/.build"] - rootEntries.append(contentsOf: baseEntries) - var iosEntries = [".DS_Store", ".build"] - iosEntries.append(contentsOf: baseEntries) - let rootGitignore = package.basePathURL.appending(path: ".gitignore") - let iOSGitignore = package.iosSrcDirectoryURL.appending(path: ".gitignore") - try modifyGitignore(at: rootGitignore, with: rootEntries) - try modifyGitignore(at: iOSGitignore, with: iosEntries) - } - - private func modifyGitignore(at fileURL: URL, with content: [String]) throws { - var gitignoreText = "" - if FileManager.default.fileExists(atPath: fileURL.path()) { - gitignoreText = try String(contentsOf: fileURL, encoding: .utf8) - } - content.forEach { - if !gitignoreText.contains($0) { - gitignoreText.append("\n\($0)") - } - } - try gitignoreText.write(to: fileURL, atomically: true, encoding: .utf8) - } - - private func moveItemCreatingIntermediaryDirectories(at: URL, to: URL) throws { - print("Moving \(at.path()) to \(to.path())...") - let parentPath = to.deletingLastPathComponent() - if !FileManager.default.fileExists(atPath: parentPath.path) { - try FileManager.default.createDirectory(at: parentPath, withIntermediateDirectories: true, attributes: nil) - } - try FileManager.default.moveItem(at: at, to: to) - } -} diff --git a/Tests/CapacitorPluginMethodTests.swift b/Tests/CapacitorPluginMethodTests.swift deleted file mode 100644 index f723531..0000000 --- a/Tests/CapacitorPluginMethodTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -@testable import cap2spm -import XCTest - -class CapacitorPluginMethodTests: XCTestCase { - func testCapacitorPluginMethod() { - let pluginMethod = CapacitorPluginMethod(methodName: "testMethod", returnType: .promise) - var methodOutput = "" - pluginMethod.functionCallExpr.write(to: &methodOutput) - XCTAssertEqual(methodOutput, "CAPPluginMethod(name: \"testMethod\", returnType: CAPPluginReturnPromise)") - } -} diff --git a/Tests/CapacitorPluginSyntaxToolsTests/CapacitorPluginMethodTests.swift b/Tests/CapacitorPluginSyntaxToolsTests/CapacitorPluginMethodTests.swift new file mode 100644 index 0000000..a56495f --- /dev/null +++ b/Tests/CapacitorPluginSyntaxToolsTests/CapacitorPluginMethodTests.swift @@ -0,0 +1,18 @@ +import Testing +import Foundation +@testable import CapacitorPluginSyntaxTools + +struct CapacitorPluginMethodTests { + let method: CapacitorPluginMethod + + init() { + method = CapacitorPluginMethod(methodName: "testMethod", returnType: .promise) + } + + @Test("Method is created correctly") + func findsPodSpec() async throws { + var methodOutput = "" + method.syntax.functionCallExpr.write(to: &methodOutput) + #expect(methodOutput == "CAPPluginMethod(name: \"testMethod\", returnType: CAPPluginReturnPromise)") + } +} diff --git a/Tests/CapacitorPluginToolsTests/PackageFileGenerator.swift b/Tests/CapacitorPluginToolsTests/PackageFileGenerator.swift new file mode 100644 index 0000000..3d3c831 --- /dev/null +++ b/Tests/CapacitorPluginToolsTests/PackageFileGenerator.swift @@ -0,0 +1,47 @@ +import Foundation +import Testing +@testable import CapacitorPluginTools + +struct PackageFileGeneratorTests { + let packageFileGenerator: PackageFileGenerator + + init() { + packageFileGenerator = PackageFileGenerator(packageName: "CapacitorAppPlugin", libName: "AppPlugin") + } + + @Test("Generates expected Package.swift Text") + func generatePackageSwiftContent() async throws { + #expect(packageFileGenerator.packageText == expected) + } + + let expected = """ + // swift-tools-version: 5.9 + import PackageDescription + + let package = Package( + name: "CapacitorAppPlugin", + platforms: [.iOS(.v14)], + products: [ + .library( + name: "AppPlugin", + targets: ["AppPlugin"]) + ], + dependencies: [ + .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0") + ], + targets: [ + .target( + name: "AppPlugin", + dependencies: [ + .product(name: "Capacitor", package: "capacitor-swift-pm"), + .product(name: "Cordova", package: "capacitor-swift-pm") + ], + path: "ios/Sources/AppPlugin"), + .testTarget( + name: "AppPluginTests", + dependencies: ["AppPlugin"], + path: "ios/Tests/AppPluginTests") + ] + ) + """ +} diff --git a/Tests/GeneratePackageFileTests.swift b/Tests/GeneratePackageFileTests.swift deleted file mode 100644 index 3e21bd3..0000000 --- a/Tests/GeneratePackageFileTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -@testable import cap2spm -import XCTest - -class GeneratePackageFileTests: XCTestCase { - func testPackageGen() { - let packageFile = GeneratePackageFile(packageName: "CapacitorAppPlugin", libName: "AppPlugin") - let expected = """ - // swift-tools-version: 5.9 - import PackageDescription - - let package = Package( - name: "CapacitorAppPlugin", - platforms: [.iOS(.v13)], - products: [ - .library( - name: "AppPlugin", - targets: ["AppPlugin"]) - ], - dependencies: [ - .package(url: "https://github.com/ionic-team/capacitor6-spm-test.git", branch: "main") - ], - targets: [ - .target( - name: "AppPlugin", - dependencies: [ - .product(name: "Capacitor", package: "capacitor6-spm-test"), - .product(name: "Cordova", package: "capacitor6-spm-test") - ], - path: "ios/Sources/AppPlugin"), - .testTarget( - name: "AppPluginTests", - dependencies: ["AppPlugin"], - path: "ios/Tests/AppPluginTests") - ] - ) - """ - - XCTAssertEqual(packageFile.packageText, expected) - } -} diff --git a/Tests/JavascriptPackageToolsTests/PackageJSONParserTests.swift b/Tests/JavascriptPackageToolsTests/PackageJSONParserTests.swift new file mode 100644 index 0000000..1091434 --- /dev/null +++ b/Tests/JavascriptPackageToolsTests/PackageJSONParserTests.swift @@ -0,0 +1,18 @@ +import Testing +import Foundation +import JavascriptPackageTools + +struct PackageJSONParserTests { + var packageJSONParser: PackageJSONParser + + init() throws { + let testJSON = try #require(Bundle.module.url(forResource: "package-test", withExtension: "json")) + packageJSONParser = try PackageJSONParser(with: testJSON) + } + + @Test("Correctly finds the podspec") + func findsPodSpec() async throws { + #expect(packageJSONParser.podspec == "Typical.podspec") + } + +} diff --git a/Tests/JavascriptPackageToolsTests/package-test.json b/Tests/JavascriptPackageToolsTests/package-test.json new file mode 100644 index 0000000..e2f070c --- /dev/null +++ b/Tests/JavascriptPackageToolsTests/package-test.json @@ -0,0 +1,85 @@ +{ + "name": "@capacitor/typicalplugin", + "version": "7.0.1", + "description": "Does some things", + "main": "dist/plugin.cjs.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "unpkg": "dist/plugin.js", + "files": [ + "android/src/main/", + "android/build.gradle", + "dist/", + "ios/Sources", + "ios/Tests", + "Package.swift", + "Typical.podspec" + ], + "author": "Ionic ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ionic-team/capacitor-plugins.git" + }, + "bugs": { + "url": "https://github.com/ionic-team/capacitor-plugins/issues" + }, + "keywords": [ + "capacitor", + "plugin", + "native" + ], + "scripts": { + "verify": "npm run verify:ios && npm run verify:android && npm run verify:web", + "verify:ios": "xcodebuild build -scheme TypicalPlugin -destination generic/platform=iOS", + "verify:android": "cd android && ./gradlew clean build test && cd ..", + "verify:web": "npm run build", + "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint", + "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format", + "eslint": "eslint . --ext ts", + "prettier": "prettier \"**/*.{css,html,ts,js,java}\"", + "swiftlint": "node-swiftlint", + "docgen": "docgen --api TypicalPlugin --output-readme README.md --output-json dist/docs.json", + "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs", + "clean": "rimraf ./dist", + "watch": "tsc --watch", + "prepublishOnly": "npm run build", + "publish:cocoapod": "pod trunk push ./Typical.podspec --allow-warnings" + }, + "devDependencies": { + "@capacitor/android": "next", + "@capacitor/cli": "^6.0.0", + "@capacitor/core": "next", + "@capacitor/docgen": "0.2.2", + "@capacitor/ios": "next", + "@ionic/eslint-config": "^0.4.0", + "@ionic/prettier-config": "~1.0.1", + "@ionic/swiftlint-config": "^1.1.2", + "eslint": "^8.57.0", + "prettier": "~2.3.0", + "prettier-plugin-java": "~1.0.2", + "rimraf": "^6.0.1", + "rollup": "^4.26.0", + "swiftlint": "^1.0.1", + "typescript": "~4.1.5" + }, + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + }, + "prettier": "@ionic/prettier-config", + "swiftlint": "@ionic/swiftlint-config", + "eslintConfig": { + "extends": "@ionic/eslint-config/recommended" + }, + "capacitor": { + "ios": { + "src": "ios" + }, + "android": { + "src": "android" + } + }, + "publishConfig": { + "access": "public" + } +}