Skip to content
Merged
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
11 changes: 10 additions & 1 deletion Examples/HelloWorldServer/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion Examples/StarWars/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ let package = Package(
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "4.1.0"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-syntax.git", "600.0.1"..<"603.0.0"),
.package(url: "https://github.com/jpsim/Yams.git", "4.0.0"..<"7.0.0"),
],
targets: [
// Build plugin
Expand All @@ -42,6 +43,7 @@ let package = Package(
dependencies: [
"GraphQLGeneratorCore",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Yams", package: "Yams"),
]
),
.target(
Expand All @@ -62,6 +64,12 @@ let package = Package(
"GraphQLGeneratorCore"
]
),
.testTarget(
name: "GraphQLGeneratorTests",
dependencies: [
"GraphQLGenerator"
]
),

// Macro
.macro(
Expand Down
91 changes: 57 additions & 34 deletions Plugins/GraphQLGeneratorPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,65 @@ import PackagePlugin
struct GraphQLGeneratorPlugin: BuildToolPlugin {
/// Entry point for creating build commands for targets in Swift packages.
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
// This plugin only runs for package targets that can have source files.
guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] }

// Find the GraphQL schema files
let schemaFiles = sourceFiles.filter { file in
file.url.pathExtension == "graphql" || file.url.pathExtension == "gql"
// This plugin only runs for Swift source targets.
guard let target = target as? SwiftSourceModuleTarget else {
return []
}

// If no schema files found, return early
guard !schemaFiles.isEmpty else { return [] }
// Find the config file, if present
let configFile = findConfigFile(in: target.sourceFiles)

// Find the generator tool
let generatorTool = try context.tool(named: "GraphQLGenerator")

// Create output directory for generated files
let outputDirectory = context.pluginWorkDirectoryURL

// Generate a single set of files from all schema files
// (We could also generate per-file, but typically GraphQL schemas are combined)
let schemaInputs = schemaFiles.map(\.url)

let outputFiles = [
outputDirectory.appendingPathComponent("BuildGraphQLSchema.swift"),
outputDirectory.appendingPathComponent("GraphQLRawSDL.swift"),
outputDirectory.appendingPathComponent("GraphQLTypes.swift"),
]

let arguments =
schemaInputs.flatMap { ["\($0.path)"] } + [
"--output-directory", outputDirectory.path,
]
var arguments: [String] = []

// Pass the target's source directory for fallback schema discovery
arguments += ["--source-directory", target.directoryURL.path()]

// Pass output directory
arguments += ["--output-directory", outputDirectory.path]

// Pass config file if found
if let configURL = configFile {
arguments += ["--config", configURL.path]
}

let inputFiles: [URL] = configFile.map { [$0] } ?? []

return [
.buildCommand(
displayName:
"Generating GraphQL Swift code from \(schemaFiles.count) schema file(s)",
displayName: "Generating GraphQL Swift code",
executable: generatorTool.url,
arguments: arguments,
inputFiles: schemaInputs,
inputFiles: inputFiles,
outputFiles: outputFiles
)
]
}

/// Supported config file names in the target's source directory.
private static let supportedConfigFiles: Set<String> = [
"graphql-generator-config.yaml",
"graphql-generator-config.yml",
]

/// Finds the generator config file in the target's source files, if present.
private func findConfigFile(in sourceFiles: FileList) -> URL? {
let configs = sourceFiles.map(\.url).filter {
Self.supportedConfigFiles.contains($0.lastPathComponent)
}
return configs.first
}
}

#if canImport(XcodeProjectPlugin)
Expand All @@ -58,39 +74,46 @@ struct GraphQLGeneratorPlugin: BuildToolPlugin {
func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws
-> [Command]
{
// Find GraphQL schema files
let schemaFiles = target.inputFiles.filter { file in
file.url.pathExtension == "graphql" || file.url.pathExtension == "gql"
}
// Find the config file
let configFile = findConfigFile(in: target.inputFiles)

// If no schema files found, return early
guard !schemaFiles.isEmpty else { return [] }
// Derive the source directory from the target's input files
let sourceDirectory =
target.inputFiles.first?.url.deletingLastPathComponent().path
?? context.xcodeProject.directoryURL.path

// Find the generator tool
let generatorTool = try context.tool(named: "GraphQLGenerator")

// Create output directory for generated files
let outputDirectory = context.pluginWorkDirectoryURL

let schemaInputs = schemaFiles.map(\.url)

let outputFiles = [
outputDirectory.appendingPathComponent("Types.swift"),
outputDirectory.appendingPathComponent("Schema.swift"),
]

let arguments =
schemaInputs.flatMap { ["\($0.path)"] } + [
"--output-directory", outputDirectory.path,
]
var arguments: [String] = []

// Pass the source directory for fallback schema discovery
arguments += ["--source-directory", sourceDirectory]

// Pass output directory
arguments += ["--output-directory", outputDirectory.path]

// Pass config file if found
if let configURL = configFile {
arguments += ["--config", configURL.path]
}

let inputFiles: [URL] = configFile.map { [$0] } ?? []

return [
.buildCommand(
displayName:
"Generating GraphQL Swift code from \(schemaFiles.count) schema file(s)",
displayName: "Generating GraphQL Swift code",
executable: generatorTool.url,
arguments: arguments,
inputFiles: schemaInputs,
inputFiles: inputFiles,
outputFiles: outputFiles
)
]
Expand Down
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Take a look at the example projects to see real, fully featured implementations:

Create a `.graphql` file in your target's `Sources` directory:

**Sources/ExamplePackage/schema.graphql**:
**Sources/MyTarget/schema.graphql**:
```graphql
type User {
name: String!
Expand All @@ -35,14 +35,32 @@ type Query {
}
```

### 2. Build Your Project
### 2. Add the Plugin to your Target

In your `package.swift`, add the plugin and dependencies to your GraphQL target:

```
.target(
name: "MyTarget",
dependencies: [
.product(name: "GraphQL", package: "GraphQL"),
.product(name: "GraphQLGeneratorMacros", package: "graphql-generator"),
.product(name: "GraphQLGeneratorRuntime", package: "graphql-generator"),
],
plugins: [
.plugin(name: "GraphQLGeneratorPlugin", package: "graphql-generator")
]
),
```

### 3. Build Your Project

When you build, the plugin will automatically generate Swift code. If you want, you can view it in the `.build/plugins/outputs` directory:
- `BuildGraphQLSchema.swift` - Defines `buildGraphQLSchema` function that builds an executable schema.
- `GraphQLRawSDL.swift` - The `graphQLRawSDL` global property, which is a Swift string literal of the input schema. This is used at runtime to parse the schema.
- `GraphQLTypes.swift` - Swift protocols and types for your GraphQL types. These are all namespaced within `GraphQLGenerated`.

### 3. Create required types
### 4. Create required types

Create a type named `GraphQLContext`:

Expand All @@ -57,9 +75,9 @@ If your schema has any custom scalar types, you must create them manually in the
Create a struct that conforms to `GraphQLGenerated.Resolvers` by defining the required typealiases:
```swift
struct Resolvers: GraphQLGenerated.Resolvers {
typealias Query = ExamplePackage.Query
typealias Mutation = ExamplePackage.Mutation
typealias Subscription = ExamplePackage.Subscription
typealias Query = MyTarget.Query
typealias Mutation = MyTarget.Mutation
typealias Subscription = MyTarget.Subscription
}
```

Expand Down Expand Up @@ -111,7 +129,7 @@ struct User: GraphQLGenerated.User {

Note that you must include the `GraphQLGeneratedMacros` library to use the macros.

### 4. Execute GraphQL Queries
### 5. Execute GraphQL Queries

You're done! You can now instantiate your GraphQL schema by calling `buildGraphQLSchema`, and run queries against it:

Expand All @@ -126,6 +144,12 @@ let result = try await graphql(schema: schema, request: "{ users { name email }
print(result)
```

## Configuration

You can configure this package by including a `graphql-generator-config.yaml` file in your target's source files. It supports the following fields:

`schemas: [String]?`: Paths to GraphQL schema files or directories containing schema files. These paths are relative to the sources directory, and directories are explored recursively. If not provided, the whole sources directory is searched.

## Design Philosophy

This generator is designed with the following guiding principles:
Expand Down
Loading
Loading