Martijn
Martijn

Reputation: 2324

Change where certain generated resources are placed

I currently have two projects; one for the server-side (Akka-HTTP) and one for the client-side (ScalaJS). The client-side generates the .js files I want to serve using the server-side. To give the server-side access to those files, I added this build.sbt entry in the server-side project:

lazy val server = project.in(file("server"))
  ...
  .settings(
    (resourceGenerators in Compile) <+= Def.task {
      val f1 = (fullOptJS in client).value.data
      val f1sm = f1.getParentFile / (f1.getName + ".map")
      f1 :: f1sm :: (packageMinifiedJSDependencies in client).value :: Nil
    }
  )

This, however, puts my .js and .map files inside the root of the classes folder. Since I want to serve my assets like so:

pathPrefix("assets") {
  ...
  pathPrefix("client") {
    getFromResourceDirectory("<directory where .js and .map files are placed>")
  }
}

with emphasis on the #getFromResourceDirectory method, this isn't going to work with my assets inside the root of my classes folder. My application.conf is placed in the same directory as the generated files which would make that available for the whole world to see.

What I would like is my .js and .map files inside a sub-directory of the classes directory. Something like client/ so I can use the #getFromResourceDirectory method like getFromResourceDirectory("client") and don't worry my configuration file is available to the outside world.

My question - How do I achieve this?

I already tried adding:

lazy val client = project.in(file("client"))
  ...
  .settings(inConfig(Compile)((fullOptJS :: packageMinifiedJSDependencies :: Nil)
    .map(a => (crossTarget in a) ~= (_ / "client")))
  )

to my client-side project which only places the generated .js and .map files in a client folder in the client-side project its scala-2.11 folder:

client-side target overview

but the setting does not propagate to the server-side project its classes folder:

server-side target overview

This is the desired situation:

server-side target overview desired

Upvotes: 1

Views: 309

Answers (2)

Tao
Tao

Reputation: 1

@Martijn, thx for the great solution.

This is real help me a lot. little update here.

for frontend/client settings.

  .settings(inConfig(Compile)(
    (fullOptJS :: fastOptJS :: packageScalaJSLauncher
     :: packageJSDependencies :: packageMinifiedJSDependencies :: Nil)
      .map(f => (crossTarget in f) ~= (_ / "sjsout"))
  ))

for backend/server settings.

  .settings(
    (resourceGenerators in Compile) += Def.task {
      val fastJsOut = (fastOptJS in Compile in frontend).value.data
      val fastJsSourceMap = fastJsOut.getParentFile / (fastJsOut.getName + ".map")
      val fullJsOut = (fullOptJS in Compile in frontend).value.data
      val fullJsSourceMap = fullJsOut.getParentFile / (fullJsOut.getName + ".map")
      Seq(
        fastJsOut,
        fastJsSourceMap,
        //fullJsOut,
        //fullJsSourceMap,
        (packageScalaJSLauncher in Compile in frontend).value.data,
        (packageJSDependencies in Compile in frontend).value
        //(packageMinifiedJSDependencies in Compile in frontend).value
      )
    }.taskValue,
    (resourceDirectories in Compile) += (crossTarget in frontend).value,
    watchSources ++= (watchSources in frontend).value
  )

Upvotes: 0

Martijn
Martijn

Reputation: 2324

Diving into SBT let me down the following path; Firstly, I inspected server/compile:resourceGenerators (using sbt inspect server/compile:resourceGenerators):

[info] ...
[info] Dependencies:
[info]  server/compile:discoveredSbtPlugins
[info]  server/compile:resourceManaged
[info]  client/compile:packageMinifiedJSDependencies
[info]  client/compile:fullOptJS
[info] Reverse dependencies:
[info]  server/compile:managedResources
[info] ...

I then inspected server/compile:managedResources:

[info] ...
[info] Dependencies:
[info]  server/compile:resourceGenerators
[info] Reverse dependencies:
[info]  server/compile:resources
[info] ...

I then inspected server/compile:resources:

[info] ...
[info] Dependencies:
[info]  server/compile:managedResources
[info]  server/compile:unmanagedResources
[info] Reverse dependencies:
[info]  server/compile:copyResources
[info] ...

I then inspected server/compile:copyResources:

[info] ...
[info] Defined at:
[info]  (sbt.Defaults) Defaults.scala:295
[info] Dependencies:
[info]  server/compile:classDirectory
[info]  server/compile:resources
[info]  server/compile:resourceDirectories
[info]  server/compile:copyResources::streams
[info] ...

I then inspected the Defaults.scala file from SBT on line 295:

copyResources <<= copyResourcesTask

I then inspected this copyResourcesTask definition:

def copyResourcesTask =
  (classDirectory, resources, resourceDirectories, streams) map { (target, resrcs, dirs, s) =>
    val cacheFile = s.cacheDirectory / "copy-resources"
    val mappings = (resrcs --- dirs) pair (rebase(dirs, target) | flat(target))
    s.log.debug("Copy resource mappings: " + mappings.mkString("\n\t", "\n\t", ""))
    Sync(cacheFile)(mappings)
    mappings
  }

The mappings val is very important in this case. It creates the mappings between the paths created by the various tasks responsible for all the resources and the final path these resources will receive in the target folder. The #rebase method is (mostly) responsible for these mappings but it falls back on the #flat method if not a single resourceDirectories entry is in the path of the resource. By simply adding the crossTarget path of my client-side project to the resourceDirectories setting, rebase replaces everything up until the client/<filename> part of my .js and .map files instead of falling-back on the #flat method and only replacing the <filename> part which, combined with this setting:

lazy val client = project.in(file("client"))
  ...
  .settings(inConfig(Compile)((fullOptJS :: packageMinifiedJSDependencies :: Nil)
    .map(a => (crossTarget in a) ~= (_ / "client")))
  )

is exactly the behavior I want.


TL;DR

Add to your client-side project:

.settings(inConfig(Compile)((fullOptJS :: packageMinifiedJSDependencies :: Nil)
  .map(a => (crossTarget in a) ~= (_ / "client")))
)

Add to your server-side project:

.settings(inConfig(Compile)((resourceGenerators <+= Def.task {
  val f1 = (fullOptJS in client).value.data
  val f1sm = f1.getParentFile / (f1.getName + ".map")
  f1 :: f1sm :: (packageMinifiedJSDependencies in client).value :: Nil
}) :: (resourceDirectories += (crossTarget in client).value) :: Nil))

Upvotes: 3

Related Questions