diff --git a/Sources/ApolloCodegenLib/ApolloCodegen.swift b/Sources/ApolloCodegenLib/ApolloCodegen.swift index 7768b69c15..bfcf62e2d4 100644 --- a/Sources/ApolloCodegenLib/ApolloCodegen.swift +++ b/Sources/ApolloCodegenLib/ApolloCodegen.swift @@ -22,6 +22,7 @@ public class ApolloCodegen { case invalidSchemaName(_ name: String, message: String) case targetNameConflict(name: String) case typeNameConflict(name: String, conflictingName: String, containingObject: String) + case failedToComputeOperationIdentifier(type: String, name: String, error: Swift.Error) public var errorDescription: String? { switch self { @@ -64,6 +65,8 @@ public class ApolloCodegen { Recommend using a field alias for one of these fields to resolve this conflict. \ For more info see: https://www.apollographql.com/docs/ios/troubleshooting/codegen-troubleshooting#typenameconflict """ + case let .failedToComputeOperationIdentifier(type, name, error): + return "Received failure while computing operation identifier for \(type) named '\(name)', Error: \(error.localizedDescription)" } } } @@ -91,6 +94,9 @@ public class ApolloCodegen { } + /// A `nil` result is treated as a cancellation, and the default operationIdentifier is used + public typealias ComputeOperationIdentifier = (any IROperation, @escaping (Result?) -> Void) -> Void + /// Executes the code generation engine with a specified configuration. /// /// - Parameters: @@ -103,16 +109,18 @@ public class ApolloCodegen { public static func build( with configuration: ApolloCodegenConfiguration, withRootURL rootURL: URL? = nil, - itemsToGenerate: ItemsToGenerate = [.code] + itemsToGenerate: ItemsToGenerate = [.code], + computeOperationIdentifier: ComputeOperationIdentifier? = nil ) throws { - try build(with: configuration, rootURL: rootURL, itemsToGenerate: itemsToGenerate) + try build(with: configuration, rootURL: rootURL, itemsToGenerate: itemsToGenerate, computeOperationIdentifier: computeOperationIdentifier) } internal static func build( with configuration: ApolloCodegenConfiguration, rootURL: URL? = nil, fileManager: ApolloFileManager = .default, - itemsToGenerate: ItemsToGenerate + itemsToGenerate: ItemsToGenerate, + computeOperationIdentifier: ComputeOperationIdentifier? = nil ) throws { let configContext = ConfigurationContext( @@ -131,29 +139,34 @@ public class ApolloCodegen { let ir = IR(compilationResult: compilationResult) - var existingGeneratedFilePaths: Set? - - if itemsToGenerate.contains(.code) && configuration.options.pruneGeneratedFiles { - existingGeneratedFilePaths = try findExistingGeneratedFilePaths( + let generate: () throws -> Void = { + try generateFiles( + compilationResult: compilationResult, + ir: ir, config: configContext, - fileManager: fileManager + fileManager: fileManager, + itemsToGenerate: itemsToGenerate, + computeOperationIdentifier: computeOperationIdentifier ) } - try generateFiles( - compilationResult: compilationResult, - ir: ir, - config: configContext, - fileManager: fileManager, - itemsToGenerate: itemsToGenerate - ) - - if var existingGeneratedFilePaths { + let generateWithPruning: () throws -> Void = { + var existingGeneratedFilePaths = try findExistingGeneratedFilePaths( + config: configContext, + fileManager: fileManager + ) + try generate() try deleteExtraneousGeneratedFiles( from: &existingGeneratedFilePaths, afterCodeGenerationUsing: fileManager ) } + + if itemsToGenerate.contains(.code) && configuration.options.pruneGeneratedFiles { + try generateWithPruning() + } else { + try generate() + } } // MARK: Internal @@ -412,7 +425,8 @@ public class ApolloCodegen { ir: IR, config: ConfigurationContext, fileManager: ApolloFileManager = .default, - itemsToGenerate: ItemsToGenerate + itemsToGenerate: ItemsToGenerate, + computeOperationIdentifier: ComputeOperationIdentifier? = nil ) throws { if itemsToGenerate.contains(.code) { @@ -431,9 +445,40 @@ public class ApolloCodegen { operationIDsFileGenerator = OperationManifestFileGenerator(config: config) } - for operation in compilationResult.operations { + let irOperations = compilationResult.operations.map { ir.build(operation: $0) } + var results = [Result?](repeating: nil, count: irOperations.count) + + if let computeOperationIdentifier { + let dispatchGroup = DispatchGroup() + DispatchQueue.concurrentPerform(iterations: irOperations.count) { index in + let irOperation = irOperations[index] + var sources: [String] = [irOperation.definition.source.convertedToSingleLine()] + for fragment in irOperation.referencedFragments { + sources.append(fragment.definition.source.convertedToSingleLine()) + } + dispatchGroup.enter() + computeOperationIdentifier(irOperation) { result in + results[index] = result + dispatchGroup.leave() + } + } + dispatchGroup.wait() + } + + for (index, irOperation) in irOperations.enumerated() { try autoreleasepool { - let irOperation = ir.build(operation: operation) + if let result = results[index] { + switch result { + case .success(let operationIdentifier): + irOperation.operationIdentifier = operationIdentifier + case .failure(let error): + throw Error.failedToComputeOperationIdentifier( + type: irOperation.definition.operationType.rawValue, + name: irOperation.definition.name, + error: error + ) + } + } if itemsToGenerate.contains(.code) { try validateTypeConflicts(for: irOperation.rootField.selectionSet, with: config, in: irOperation.definition.name) @@ -611,4 +656,24 @@ public class ApolloCodegen { } +public protocol IROperation: AnyObject { + var filePath: String { get } + var name: String { get } + var source: String { get } + var type: CompilationResult.OperationType { get } +} + +extension IR.Operation: IROperation { + public var filePath: String { definition.filePath } + public var name: String { definition.name } + public var source: String { + var sources: [String] = [definition.source.convertedToSingleLine()] + for fragment in referencedFragments { + sources.append(fragment.definition.source.convertedToSingleLine()) + } + return sources.joined(separator: "\n") + } + public var type: CompilationResult.OperationType { definition.operationType } +} + #endif diff --git a/Sources/ApolloCodegenLib/Frontend/CompilationResult.swift b/Sources/ApolloCodegenLib/Frontend/CompilationResult.swift index 335ed22799..fb6d9aedc0 100644 --- a/Sources/ApolloCodegenLib/Frontend/CompilationResult.swift +++ b/Sources/ApolloCodegenLib/Frontend/CompilationResult.swift @@ -1,7 +1,7 @@ import JavaScriptCore /// The output of the frontend compiler. -public class CompilationResult: JavaScriptObject { +public final class CompilationResult: JavaScriptObject { private enum Constants { static let LocalCacheMutationDirectiveName = "apollo_client_ios_localCacheMutation" } @@ -15,7 +15,7 @@ public class CompilationResult: JavaScriptObject { lazy var schemaDocumentation: String? = self["schemaDocumentation"] - public class RootTypeDefinition: JavaScriptObject { + public final class RootTypeDefinition: JavaScriptObject { lazy var queryType: GraphQLNamedType = self["queryType"] lazy var mutationType: GraphQLNamedType? = self["mutationType"] @@ -23,7 +23,7 @@ public class CompilationResult: JavaScriptObject { lazy var subscriptionType: GraphQLNamedType? = self["subscriptionType"] } - public class OperationDefinition: JavaScriptObject, Hashable { + public final class OperationDefinition: JavaScriptObject, Hashable { lazy var name: String = self["name"] lazy var operationType: OperationType = self["operationType"] @@ -94,7 +94,7 @@ public class CompilationResult: JavaScriptObject { } } - public class VariableDefinition: JavaScriptObject { + public final class VariableDefinition: JavaScriptObject { lazy var name: String = self["name"] lazy var type: GraphQLType = self["type"] @@ -102,7 +102,7 @@ public class CompilationResult: JavaScriptObject { lazy var defaultValue: GraphQLValue? = self["defaultValue"] } - public class FragmentDefinition: JavaScriptObject, Hashable { + public final class FragmentDefinition: JavaScriptObject, Hashable { lazy var name: String = self["name"] lazy var type: GraphQLCompositeType = self["typeCondition"] @@ -132,7 +132,7 @@ public class CompilationResult: JavaScriptObject { } } - public class SelectionSet: JavaScriptWrapper, Hashable, CustomDebugStringConvertible { + public final class SelectionSet: JavaScriptWrapper, Hashable, CustomDebugStringConvertible { lazy var parentType: GraphQLCompositeType = self["parentType"] lazy var selections: [Selection] = self["selections"] @@ -165,7 +165,7 @@ public class CompilationResult: JavaScriptObject { } } - public class InlineFragment: JavaScriptObject, Hashable { + public final class InlineFragment: JavaScriptObject, Hashable { lazy var selectionSet: SelectionSet = self["selectionSet"] lazy var inclusionConditions: [InclusionCondition]? = self["inclusionConditions"] @@ -187,7 +187,7 @@ public class CompilationResult: JavaScriptObject { /// Represents an individual selection that includes a named fragment in a selection set. /// (ie. `...FragmentName`) - public class FragmentSpread: JavaScriptObject, Hashable { + public final class FragmentSpread: JavaScriptObject, Hashable { lazy var fragment: FragmentDefinition = self["fragment"] lazy var inclusionConditions: [InclusionCondition]? = self["inclusionConditions"] @@ -253,7 +253,7 @@ public class CompilationResult: JavaScriptObject { } } - public class Field: JavaScriptWrapper, Hashable, CustomDebugStringConvertible { + public final class Field: JavaScriptWrapper, Hashable, CustomDebugStringConvertible { lazy var name: String = self["name"]! lazy var alias: String? = self["alias"] @@ -330,7 +330,7 @@ public class CompilationResult: JavaScriptObject { } } - public class Argument: JavaScriptObject, Hashable { + public final class Argument: JavaScriptObject, Hashable { lazy var name: String = self["name"] lazy var type: GraphQLType = self["type"] @@ -352,7 +352,7 @@ public class CompilationResult: JavaScriptObject { } } - public class Directive: JavaScriptObject, Hashable { + public final class Directive: JavaScriptObject, Hashable { lazy var name: String = self["name"] lazy var arguments: [Argument]? = self["arguments"] diff --git a/Sources/ApolloCodegenLib/IR/IR.swift b/Sources/ApolloCodegenLib/IR/IR.swift index b19724aae3..ebeb8fd584 100644 --- a/Sources/ApolloCodegenLib/IR/IR.swift +++ b/Sources/ApolloCodegenLib/IR/IR.swift @@ -1,7 +1,7 @@ import OrderedCollections import CryptoKit -class IR { +final class IR { let compilationResult: CompilationResult @@ -85,7 +85,7 @@ class IR { /// /// Multiple `SelectionSet`s may select fields on the same `Entity`. All `SelectionSet`s that will /// be selected on the same object share the same `Entity`. - class Entity { + final class Entity { /// Represents the location within a GraphQL definition (operation or fragment) of an `Entity`. struct Location: Hashable { @@ -170,7 +170,7 @@ class IR { } } - class Operation { + final class Operation { let definition: CompilationResult.OperationDefinition /// The root field of the operation. This field must be the root query, mutation, or @@ -218,7 +218,7 @@ class IR { } } - class NamedFragment: Hashable, CustomDebugStringConvertible { + final class NamedFragment: Hashable, CustomDebugStringConvertible { let definition: CompilationResult.FragmentDefinition let rootField: EntityField @@ -264,7 +264,7 @@ class IR { /// Represents an Inline Fragment that has been "spread into" another SelectionSet using the /// spread operator (`...`). - class InlineFragmentSpread: Hashable, CustomDebugStringConvertible { + final class InlineFragmentSpread: Hashable, CustomDebugStringConvertible { /// The `SelectionSet` representing the inline fragment that has been "spread into" its /// enclosing operation/fragment. let selectionSet: SelectionSet @@ -310,7 +310,7 @@ class IR { /// /// While a `NamedFragment` can be shared between operations, a `NamedFragmentSpread` represents a /// `NamedFragment` included in a specific operation. - class NamedFragmentSpread: Hashable, CustomDebugStringConvertible { + final class NamedFragmentSpread: Hashable, CustomDebugStringConvertible { /// The `NamedFragment` that this fragment refers to. /// diff --git a/Sources/CodegenCLI/Commands/Generate.swift b/Sources/CodegenCLI/Commands/Generate.swift index e907beddb4..f3912f7278 100644 --- a/Sources/CodegenCLI/Commands/Generate.swift +++ b/Sources/CodegenCLI/Commands/Generate.swift @@ -77,7 +77,8 @@ public struct Generate: ParsableCommand { try codegenProvider.build( with: configuration, withRootURL: rootOutputURL(for: inputs), - itemsToGenerate: itemsToGenerate + itemsToGenerate: itemsToGenerate, + computeOperationIdentifier: nil ) } diff --git a/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift b/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift index 2b695766cc..77f62a3460 100644 --- a/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift +++ b/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift @@ -44,7 +44,8 @@ public struct GenerateOperationManifest: ParsableCommand { try codegenProvider.build( with: configuration, withRootURL: rootOutputURL(for: inputs), - itemsToGenerate: [.operationManifest] + itemsToGenerate: [.operationManifest], + computeOperationIdentifier: nil ) } diff --git a/Sources/CodegenCLI/Protocols/CodegenProvider.swift b/Sources/CodegenCLI/Protocols/CodegenProvider.swift index 96ad36a103..7deace8994 100644 --- a/Sources/CodegenCLI/Protocols/CodegenProvider.swift +++ b/Sources/CodegenCLI/Protocols/CodegenProvider.swift @@ -6,7 +6,8 @@ public protocol CodegenProvider { static func build( with configuration: ApolloCodegenConfiguration, withRootURL rootURL: URL?, - itemsToGenerate: ApolloCodegen.ItemsToGenerate + itemsToGenerate: ApolloCodegen.ItemsToGenerate, + computeOperationIdentifier: ApolloCodegen.ComputeOperationIdentifier? ) throws } diff --git a/Tests/CodegenCLITests/Support/MockApolloCodegen.swift b/Tests/CodegenCLITests/Support/MockApolloCodegen.swift index 24592d7ba6..fd29f2e603 100644 --- a/Tests/CodegenCLITests/Support/MockApolloCodegen.swift +++ b/Tests/CodegenCLITests/Support/MockApolloCodegen.swift @@ -8,7 +8,8 @@ class MockApolloCodegen: CodegenProvider { static func build( with configuration: ApolloCodegenConfiguration, withRootURL rootURL: URL?, - itemsToGenerate: ApolloCodegen.ItemsToGenerate + itemsToGenerate: ApolloCodegen.ItemsToGenerate, + computeOperationIdentifier: ApolloCodegen.ComputeOperationIdentifier? = nil ) throws { guard let handler = buildHandler else { fatalError("You must set buildHandler before calling \(#function)!")