diff --git a/Package.resolved b/Package.resolved index 47ef6699e..8b823b0bf 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" + "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", + "version" : "1.0.6" } } ], diff --git a/Package.swift b/Package.swift index 65b2d3ead..0fb099fe7 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ let package = Package( .upToNextMajor(from: "1.0.0")), .package( url: "https://github.com/apple/swift-collections", - .upToNextMajor(from: "1.0.0")), + .upToNextMajor(from: "1.0.6")), .package( url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.2.0")), diff --git a/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift b/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift index 4edac11f3..62f0d4839 100644 --- a/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift +++ b/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift @@ -10,7 +10,9 @@ struct FragmentTemplate: TemplateRenderer { let config: ApolloCodegen.ConfigurationContext - let target: TemplateTarget = .operationFile + var target: TemplateTarget { + .operationFile(moduleImports: fragment.definition.moduleImports) + } func renderBodyTemplate( nonFatalErrorRecorder: ApolloCodegen.NonFatalError.Recorder diff --git a/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift b/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift index 023af0772..0698f2d99 100644 --- a/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift +++ b/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift @@ -8,7 +8,9 @@ struct LocalCacheMutationDefinitionTemplate: OperationTemplateRenderer { let config: ApolloCodegen.ConfigurationContext - let target: TemplateTarget = .operationFile + var target: TemplateTarget { + .operationFile(moduleImports: operation.definition.moduleImports) + } func renderBodyTemplate( nonFatalErrorRecorder: ApolloCodegen.NonFatalError.Recorder diff --git a/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift b/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift index b40f141e1..eb4877c9f 100644 --- a/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift +++ b/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift @@ -14,7 +14,9 @@ struct OperationDefinitionTemplate: OperationTemplateRenderer { let config: ApolloCodegen.ConfigurationContext - let target: TemplateTarget = .operationFile + var target: TemplateTarget { + .operationFile(moduleImports: operation.definition.moduleImports) + } func renderBodyTemplate( nonFatalErrorRecorder: ApolloCodegen.NonFatalError.Recorder @@ -45,7 +47,7 @@ struct OperationDefinitionTemplate: OperationTemplateRenderer { """) } - + private func OperationDeclaration() -> TemplateString { return """ \(accessControlModifier(for: .parent))\ diff --git a/Sources/ApolloCodegenLib/Templates/TemplateRenderer.swift b/Sources/ApolloCodegenLib/Templates/TemplateRenderer.swift index 8e3707225..fd0118c72 100644 --- a/Sources/ApolloCodegenLib/Templates/TemplateRenderer.swift +++ b/Sources/ApolloCodegenLib/Templates/TemplateRenderer.swift @@ -1,4 +1,5 @@ import TemplateString +import OrderedCollections // MARK: TemplateRenderer @@ -7,7 +8,7 @@ enum TemplateTarget: Equatable { /// Used in schema types files; enum, input object, union, etc. case schemaFile(type: SchemaFileType) /// Used in operation files; query, mutation, fragment, etc. - case operationFile + case operationFile(moduleImports: OrderedSet? = nil) /// Used in files that define a module; Swift Package Manager, etc. case moduleFile /// Used in test mock files; schema object `Mockable` extensions @@ -23,7 +24,7 @@ enum TemplateTarget: Equatable { case customScalar case inputObject - var namespaceComponent: String? { + var namespaceComponent: String? { switch self { case .schemaMetadata, .enum, .customScalar, .inputObject, .schemaConfiguration: return nil @@ -102,10 +103,17 @@ extension TemplateRenderer { let body = { switch target { - case let .schemaFile(type): return renderSchemaFile(type, errorRecorder) - case .operationFile: return renderOperationFile(errorRecorder) - case .moduleFile: return renderModuleFile(errorRecorder) - case .testMockFile: return renderTestMockFile(errorRecorder) + case let .schemaFile(type): + return renderSchemaFile(type, errorRecorder) + + case let .operationFile(moduleImports): + return renderOperationFile(moduleImports, errorRecorder) + + case .moduleFile: + return renderModuleFile(errorRecorder) + + case .testMockFile: + return renderTestMockFile(errorRecorder) } }() @@ -153,12 +161,14 @@ extension TemplateRenderer { } private func renderOperationFile( + _ moduleImports: OrderedSet?, _ errorRecorder: ApolloCodegen.NonFatalError.Recorder ) -> String { TemplateString( """ \(ifLet: renderHeaderTemplate(nonFatalErrorRecorder: errorRecorder), { "\($0)\n" }) \(ImportStatementTemplate.Operation.template(for: config)) + \(ifLet: moduleImports, { "\(ModuleImportStatementTemplate.template(moduleImports: $0))" }) \(if: config.output.operations.isInModule && !config.output.schemaTypes.isInModule, renderBodyTemplate(nonFatalErrorRecorder: errorRecorder) @@ -350,6 +360,20 @@ struct ImportStatementTemplate { } } +/// Provides the format to import additional Swift modules required by the template type. +/// These are custom import statements defined using the `@import(module:)` directive. +struct ModuleImportStatementTemplate { + + static func template( + moduleImports: OrderedSet + ) -> TemplateString { + return """ + \(moduleImports.map { "import \($0)" }.joined(separator: "\n")) + """ + } + +} + fileprivate extension ApolloCodegenConfiguration { var schemaModuleName: String { switch output.schemaTypes.moduleType { diff --git a/Sources/GraphQLCompiler/CompilationResult.swift b/Sources/GraphQLCompiler/CompilationResult.swift index ebf5aafb4..24165a7dc 100644 --- a/Sources/GraphQLCompiler/CompilationResult.swift +++ b/Sources/GraphQLCompiler/CompilationResult.swift @@ -1,5 +1,6 @@ import JavaScriptCore import TemplateString +import OrderedCollections /// The output of the frontend compiler. public final class CompilationResult: JavaScriptObjectDecodable { @@ -7,6 +8,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { /// String constants used to match JavaScriptObject instances. fileprivate enum Constants { enum DirectiveNames { + static let Import = "import" static let LocalCacheMutation = "apollo_client_ios_localCacheMutation" static let Defer = "defer" } @@ -108,6 +110,8 @@ public final class CompilationResult: JavaScriptObjectDecodable { public let filePath: String public let isLocalCacheMutation: Bool + + public let moduleImports: OrderedSet static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { self.init( @@ -145,6 +149,9 @@ public final class CompilationResult: JavaScriptObjectDecodable { self.filePath = filePath self.isLocalCacheMutation = directives? .contains { $0.name == Constants.DirectiveNames.LocalCacheMutation } ?? false + + self.moduleImports = OperationDefinition.getImportModuleNames(directives: directives, + referencedFragments: referencedFragments) } public var debugDescription: String { @@ -158,6 +165,17 @@ public final class CompilationResult: JavaScriptObjectDecodable { public static func ==(lhs: OperationDefinition, rhs: OperationDefinition) -> Bool { return lhs.name == rhs.name } + + private static func getImportModuleNames(directives: [Directive]?, + referencedFragments: [FragmentDefinition]) -> OrderedSet { + let referencedImports: [String] = referencedFragments + .flatMap { $0.moduleImports } + let directiveImports: [String] = directives? + .compactMap { ImportDirective(directive: $0)?.moduleName } ?? [] + var orderedImports = OrderedSet(referencedImports + directiveImports) + orderedImports.sort() + return orderedImports + } } public enum OperationType: String, Equatable, Sendable, JavaScriptValueDecodable { @@ -214,15 +232,19 @@ public final class CompilationResult: JavaScriptObjectDecodable { public var isLocalCacheMutation: Bool { directives?.contains { $0.name == Constants.DirectiveNames.LocalCacheMutation } ?? false } + + public let moduleImports: OrderedSet init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { self.name = jsValue["name"] + self.directives = .fromJSValue(jsValue["directives"], bridge: bridge) self.type = .fromJSValue(jsValue["typeCondition"], bridge: bridge) self.selectionSet = .fromJSValue(jsValue["selectionSet"], bridge: bridge) - self.directives = .fromJSValue(jsValue["directives"], bridge: bridge) self.referencedFragments = .fromJSValue(jsValue["referencedFragments"], bridge: bridge) self.source = jsValue["source"] self.filePath = jsValue["filePath"] + self.moduleImports = FragmentDefinition.getImportModuleNames(directives: directives, + referencedFragments: referencedFragments) } /// Initializer to be used for creating mock objects in tests only. @@ -242,6 +264,8 @@ public final class CompilationResult: JavaScriptObjectDecodable { self.referencedFragments = referencedFragments self.source = source self.filePath = filePath + self.moduleImports = FragmentDefinition.getImportModuleNames(directives: directives, + referencedFragments: referencedFragments) } public var debugDescription: String { @@ -255,6 +279,18 @@ public final class CompilationResult: JavaScriptObjectDecodable { public static func ==(lhs: FragmentDefinition, rhs: FragmentDefinition) -> Bool { return lhs.name == rhs.name } + + private static func getImportModuleNames(directives: [Directive]?, + referencedFragments: [FragmentDefinition]) -> OrderedSet { + let referencedImports: [String] = referencedFragments + .flatMap { $0.moduleImports } + let directiveImports: [String] = directives? + .compactMap { ImportDirective(directive: $0)?.moduleName } ?? [] + + var orderedImports = OrderedSet(referencedImports + directiveImports) + orderedImports.sort() + return orderedImports + } } public final class SelectionSet: @@ -527,7 +563,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { } } - public struct Argument: + public struct Argument: JavaScriptObjectDecodable, Sendable, Hashable { public let name: String @@ -650,7 +686,34 @@ public final class CompilationResult: JavaScriptObjectDecodable { return string } } - + + fileprivate struct ImportDirective: Hashable, CustomDebugStringConvertible, Sendable, Equatable { + /// String constants used to match JavaScriptObject instances. + enum Constants { + enum ArgumentNames { + static let Module = "module" + } + } + + let moduleName: String + + init?(directive: Directive) { + guard directive.name == CompilationResult.Constants.DirectiveNames.Import else { + return nil + } + guard let moduleArgument = directive.arguments?.first( + where: { $0.name == Constants.ArgumentNames.Module }), + case let .string(moduleValue) = moduleArgument.value + else { + return nil + } + moduleName = moduleValue + } + + var debugDescription: String { + return "@import(module: \"\(moduleName)\")" + } + } } fileprivate protocol Deferrable { }