How to use `embeddedInTarget` option without adding namespacing to existing SPM package

We’re using the embeddedInTarget option when using code generation to add code to an existing SPM package target.

Our client package file looks like this:

// swift-tools-version:5.9

import PackageDescription

let package = Package(
    name: "Client",
    platforms: [
        .iOS(.v17),
    ],
    products: [
        .library(
            name: "Client",
            targets: ["Client"]
        ),
        .library(
            name: "ClientAPI",
            targets: ["ClientAPI"]
        ),
    ],
    dependencies: [
        .package(
            url: "https://github.com/apollographql/apollo-ios.git",
            .upToNextMajor(from: "1.0.0")
        ),
    ],
    targets: [
        .target(
            name: "Client",
            dependencies: [
                "ClientAPI",
                .product(name: "Apollo", package: "apollo-ios"),
            ]
        ),
        .target(
            name: "ClientAPI",
            dependencies: [
                .product(name: "Apollo", package: "apollo-ios"),
            ]
        ),
    ]
)

We then run the generate option which places the files in the Sources/ClientAPI directory of the target already defined in our package.

The issue we’re seeing is that the generated types all have import ClientAPI which leads to dozens of warnings like:

File ‘SomeFragment.graphql.swift’ is part of module ‘ClientAPI’; ignoring import

Even though the files are already in that target.

And worse still, the types sit in a namespace even though the files are already namespaced by the SPM target’s module, which means on the app side we have types that can be referenced by the fully qualified name like ClientAPI.ClientAPI.SomeFragment

ClientAPI is the the SPM module name, the second ClientAPI is the Apollo-generated enum namespace and SomeFragment is the type in question.

Any way we can avoid this?

Hi @cameroncooke, can you share your codegen configuration please. That would help to better diagnose how the generated package is being output. Thanks.

Sure thing,

{
  "schemaNamespace" : "ClientAPI",
  "input" : {
    "operationSearchPaths" : [
      "GraphQL/**/*.graphql"
    ],
    "schemaSearchPaths" : [
      "GraphQL/**/*.graphqls"
    ]
  },
  "output" : {
    "testMocks" : {
      "none" : {
      }
    },
    "schemaTypes" : {
      "path" : "Sources/ClientAPI",
      "moduleType" : {
          "embeddedInTarget" : {
            "accessModifier" : "public",
            "name" : "ClientAPI"
        }
    }
  },
  "operations" : {
    "absolute" : {
          "accessModifier" : "public",
          "path" : "Sources/ClientAPI"
      }
    }
  },
  "schemaDownload": {
      "downloadMethod": {
          "introspection": {
              "endpointURL": "http://127.0.0.1:8080/graphql",
              "httpMethod": {
                  "POST": {}
              },
              "includeDeprecatedInputValues": false,
              "outputFormat": "SDL"
          }
      },
      "downloadTimeout": 60,
      "headers": [],
      "outputPath": "GraphQL/schema.graphqls"
  } 
}

SPM package directory structure:

├── GraphQL
│   ├── Fragments
│   ├── Mutations
│   └── Queries
└── Sources
    ├── Client
    └── ClientAPI
        ├── CustomScalars
        ├── Enums
        ├── Fragments
        ├── InputObjects
        ├── Mutations
        ├── Objects
        └── Queries

OK, it looks like a couple different things are fighting against each other in this configuration:

  1. schemaNamespace needs to be distinct from the target you’re embedding into. If you look at the docs for that configuration property it states that the value given there is used as the namespace (caseless enum). We create this namespace when you’re embedding the schema types into another target to avoid type conflicts. There is no way to disable this behaviour and it’s why you’re seeing things like ClientAPI.ClientAPI. Using a different value such as GraphAPI with give you something like ClientAPI.GraphAPI instead.
  2. Your operation files are configured with absolute which the codegen engine assumes to mean that it’s in a different package, which is why you’re getting additional import ClientAPI when they’re already in the same package by the absolute path being used. If you instead use inSchemaModule there it should resolve those imports and still be in the same package although the location may change slightly.

I think those changes should resolve your issues.

The key thing to realize here is that your package, the schema module and the operation output are all distinct groups of code but in your configuration above they’re all being grouped as one through the common output path. It takes a bit of mix-and-match to get them all playing nicely.

Thanks for this, will try that configuration option and see if it resolves my issue.

On the namespace issue sure I can change the namespace name but that’s just semantics, technically I am embedding in a target, it’s just the target is a SPM target not an Xcode Project App target. Should the code gen not support disabling the namespace feature for this reason, I believe the old code gen namespace was an opt-in option?

On the namespace issue sure I can change the namespace name but that’s just semantics, technically I am embedding in a target, it’s just the target is a SPM target not an Xcode Project App target.

You’re correct, it doesn’t matter whether it’s SPM or Xcode the target is treated the same. We scope the schema output within a namespace to avoid type name conflicts. It just happens that in your code the target will only contain those types so no potential conflict. Other use cases are not that simple.

You could look into the other module type which gives you more control than the other two options.

Should the code gen not support disabling the namespace feature for this reason, I believe the old code gen namespace was an opt-in option?

No, it’s no longer configurable.

Thanks so much for your help @calvincestari the import issue has gone away.

No, it’s no longer configurable.

I guess my point was given we both agree that SPM targets are a perfectly valid target in which to generate the GraphQL types the addition of a namespace is unwarranted in this case. I feel this proves there is a need for finer granularity here.

I think other is the finer granularity option but I can’t recall the exact behaviour re. the namespace for other; would need to test it.

I kind of discounted that option as it would seem uncoupled from the namespace property of the configuration API. Maybe I’ll give it another look.