sbt plugin to generate apollo graphql scala sources

sbt plugin to generate apollo graphql scala sources

I modified the apollo-codegen-scala sources to output generic scala sources. At the moment, it is dependent on slinky. With my changes it is not dependent on slinky.

Along the way, I created a plugin to generate the sources from the npx apollo codegen:generate command. Since it is my second plugin I’ve ever written, it took forever to write. There is an add occurance in that the JvmPlugin reset the sourceGenerators setting. Tracknig that down cost me a day of time.

Anyway, the plugin is below and I’ll try to turn it into a real plugin shortly. For the moment, just copy this into a .scala file such as [your project]/project/apollo_codegen.scala and add the plugin to your project. Read the keys below to see what settings are available. If you need see the actual command generated, use the sbt last command or turn on debugging for this task via [your project]/Compile/apolloCodeGenerator/logLevel := Level.Debug. It’s not fully tested but let me know if you run into problems with it.

package apollo_codegen

import sbt._
import Keys._
import nio.Keys._
import plugins._

/**
 * Use the apollo tooling CLI to generate scala code from graphql schema and
 * operation files.  Assume nodejs apollo is installed.
 */
object ApolloCodegenPlugin extends AutoPlugin {
  override def requires = JvmPlugin // needed since JvmPlugin resets sourceGenerator :-(

  object autoImport {
    val apolloCodeGenerator = taskKey[Seq[File]]("Convert graphql to scala source files.")
    val graphQLInputSources = settingKey[Seq[Glob]]("List of graphql input sources globs. The files must contain operation definitions: queries, mutations and subscriptions.")
    val graphQLPackageName = settingKey[String]("Output package namespace. Defaults to normalized project name.")
    val graphQLSchemaFile = settingKey[File]("Graphql schema json file. Typically generated by querying a graphql endpoint.")
    val apolloCodegenOptions = settingKey[Seq[String]]("Additional settings for running apollo codegen. Added after the default CLI options.")
    val apolloCodegenTarget = settingKey[String]("--target setting for apollo command. Defaults to scala.")
  }
  import autoImport._

  // goes into ThisBuild
  override def buildSettings = Seq(
    graphQLSchemaFile := baseDirectory.value / "schema.json",
    apolloCodegenOptions := Seq(),
    apolloCodegenTarget := "scala"
  )

  // per project
  override def projectSettings: Seq[Def.Setting[_]] = Seq(
    graphQLPackageName := normalizedName.value.replaceAll("-","_"),
    graphQLInputSources := Seq(sourceDirectory.value.toGlob / "main" / "graphql" / "*.graphql"),
    apolloCodeGenerator / fileInputs ++= graphQLInputSources.value,
  ) ++ configSettings(Compile)

  def configSettings(c: Configuration) = inConfig(c)(Seq(
    apolloCodeGenerator := apolloCodegenTask.value,
    sourceGenerators += apolloCodeGenerator.taskValue,
  ))

  //define inline in autoImport or *settings via `theTask := {` or separately like this
  private def apolloCodegenTask = Def.task {
    import scala.sys.process._
    val logger = streams.value.log
    val outdir = sourceManaged.value / "apollo"
    val output = outdir / "graphql.scala"
    val output_exists = output.exists()
    val input_files_strs = apolloCodeGenerator.inputFiles.map(_.toString)
    val command = Seq(
      "npx", "apollo", "codegen:generate",
      output.toString,
      "--includes", input_files_strs.mkString(","),
      "--localSchemaFile", graphQLSchemaFile.value.toString,
      "--namespace", graphQLPackageName.value,
      "--target", apolloCodegenTarget.value,
    ) ++ apolloCodegenOptions.value

    def run() = {
      logger.debug("Generating graphql scala source files from graphql schema files.")
      outdir.mkdirs()
      logger.debug(s"Generating managed source graphql scala file into $output")
      logger.info(s"""GraphQL input files: ${input_files_strs.mkString(", ")}""")
      logger.debug("apollo codegen CLI command:")
      logger.debug(command.mkString(" "))
      command.!
    }
    // a non-zero exit value does not seem to be generated from npx apollo
    apolloCodeGenerator.inputFileChanges match {
      case fc@FileChanges(c,d,m,u) =>
        if(fc.hasChanges || !output_exists)
          run() match {
            case 1 =>
              throw new MessageOnlyException("Error running apollo codegen command. Output may not exist or be inconsistent.")
            case _ =>
          }
      case _ =>
    }
    Seq(output)    
  }
}

Comments

Popular posts from this blog

zio layers and framework integration

typescript and react types

dotty+scala.js+async: interesting options