Martijn Martijn - 5 days ago 7
Scala Question

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

Answer

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))
Comments