goodcover / scala-relay   0.45.0

GitHub
Scala versions: 2.13 2.12
Scala.js versions: 1.x
sbt plugins: 1.x

scala-relay

Relay Modern Tools for Scala.js Folks

There are three parts to this:

  • sbt-scala-relay - Adds sbt tasks to run the relay-compiler and generate Scala.js facades (relayCompiler and relayConvert).

    addSbtPlugin("com.goodcover.relay" % "sbt-scala-relay" % "<version>")
  • scala-relay-core - Contains the Scala.js equivalent to relay types as well as the @graphql annotation which allows GraphQL definitions to be defined inline.

    libraryDependencies += "com.goodcover.relay" %%% "scala-relay-core" % "<version>"

    ℹ️ This is added automatically by sbt-scala-relay.

  • scala-relay-macros - Contains the graphqlGen macro which is similar to the @graphql annotation but is able to return the type of the GraphQL definition. This also includes an IntelliJ plugin that makes the types available to the IDE.

    libraryDependencies += "com.goodcover.relay" %%% "scala-relay-macros" % "<version>"

    ℹ️ This has to be added manually.

Usage

Add the sbt-scala-relay sbt plugin to project/plugins.sbt:

addSbtPlugin("com.goodcover.relay" % "sbt-scala-relay" % "<version>")

Enable the ScalaRelayPlugin on your frontend project:

enablePlugins(ScalaRelayPlugin)

Add the path to the schema:

relaySchema := resourceDirectory.value / "graphql" / "schema.graphqls"

Optionally specify the command to run relay-compiler and include a dependency on the task that installs it. For example, if you are using scalajs-bundler add:

relayCompilerCommand := s"node ${(Compile / npmInstallDependencies).value}/node_modules/.bin/relay-compiler"

Add the relay dependencies to your node package manager. For scalajs-bundler add:

Compile / npmDevDependencies ++= (Compile / relayDependencies).value

You will need to configure your bundler so that it is able to resolve @JSImport("__generated__/...") to the relayCompileDirectory. For example, for webpack you might add:

  "resolve": {
    "modules": [
        "node_modules",
        "../../resource_managed/main"
    ]
  }

How it Works

The first task is relayExtract which processes all the @graphql annotations and graphqlGen macros within your Scala sources (unmanagedSources) and extracts the GraphQL definitions into their own .graphql file.

The second task is relayWrap which takes all the extracted .graphql files as well as any others from unmanagedResources and wraps them with the graphql tagged template and outputs them as .js files (or .ts if relayTypeScript := true). We do this because relay-compiler is rather fussy about where the executable definitions come from. The only reliable way to get it to work is to have all your executable definitions come from the same type of file as the target language.

The final two tasks, relayConvert and relayCompile, are the main tasks you will interact with and they each depend on the output of tasks above, relayExtract and relayWrap, respectively.

relayConvert takes all the GraphQL definitions and generates Scala.js facades for them. These are equivalent to the TypeScript or Flow types that relay-compiler produces.

relayCompile takes all the GraphQL definitions wrapped with the graphql tagged template and passes those to relay-compiler. The resulting sources are imported via @JSImport("__generated__/...") annotations from the Scala.js facades generated by relayConvert.

Multi-project Setup

For an example on multi-project setup, with GraphQL definitions from one project used by another, take a look at the multi-project sbt scripted test.

You need to make relayConvert aware of the GraphQL definition from the dependency by setting:

Compile / relayGraphQLDependencies ++= (dependencyProject / Compile / relayGraphQLFiles).value

ℹ️ This will add a task dependency on dependencyProject / Compile / relayExtract.

Then make relayCompile aware of the unmanaged GraphQL definitions:

Compile / relayInclude ++= {
  val base = relayBaseDirectory.value.toPath
  (dependencyProject / Compile / unmanagedResourceDirectories).value.map { dir =>
    base.relativize(dir.toPath).toString + "/**"
  }
}

As well as the extracted ones:

Compile / relayInclude +=
  relayBaseDirectory.value.toPath.relativize((dependencyProject / Compile / relayExtractDirectory).value.toPath).toString + "/**"

This does not add a task dependency on dependencyProject / Compile / relayExtract. Although technically only relayExtra is required, it is more convenient to depend on relayCompile since you will need to run it at some point before bundling the dependent project:

Compile / relayCompile := ((Compile / relayCompile) dependsOn (dependencyProject / Compile / relayCompile)).value

Do not forget to update your bundler configuration to also include the relayCompileDirectory of the dependency:

  "resolve": {
    "modules": [
        "node_modules",
        "../../resource_managed/main",
        "../../../../../../dependencyProject/target/scala-2.13/resource_managed/main",
    ]
  }

ℹ️ You should probably use path.resolve against the base directory but that is an exercise for you to figure out.