Skip to content

Instantly share code, notes, and snippets.

@jroper
Last active May 19, 2021 15:00
Show Gist options
  • Save jroper/387b05830044d006eb231abd1bc768e5 to your computer and use it in GitHub Desktop.
Save jroper/387b05830044d006eb231abd1bc768e5 to your computer and use it in GitHub Desktop.
Play Angular 2 integration

Angular 2 Play dev mode integration

This is a working Angular 2 run hook for Play Framework. To use it, put the file below in your project directory, and then add the following to your build.sbt:

lazy val angular2BaseDir = settingKey[File]("Base directory for Angular 2 app")
angular2BaseDir := <insert your base directory here> // eg, baseDirectory.value / "my-angular-app"

PlayKeys.playRunHooks += Angular2(streams.value.log, angular2BaseDir.value, target.value)
// Sets the Angular output directory as Play's public directory. This completely replaces the
// public directory, if you want to use this in addition to the assets in the public directory,
// then use this instead:
// unmanagedResourceDirectories in Assets += angular2BaseDir.value / "dist"
resourceDirectory in Assets := angular2BaseDir.value / "dist"

What this does is, when you start dev mode, it will run npm install on the Angular app if the package.json has changed since dev mode was last started. So it assumes you have a package.json configured to install Angular for you.

Then once dev mode is started, it will run the Angular build in watch mode, rebuilding whenever changes are made. Furthermore, the assets generated by Angular will be available on the classpath under the /public directory, just like regular sbt-web managed assets, and so can be served by the Play assets controller.

This requires @angular/cli 1.1.0 or above, this is to get the ability to turn off deleting the output path, which was necessary because otherwise Angular deletes the output path on each rebuild, which causes Play to stop watching it because it's impossible to watch a non existent directory.

import play.sbt.PlayRunHook
import sbt._
import java.net.InetSocketAddress
object Angular2 {
def apply(log: Logger, base: File, target: File): PlayRunHook = {
object Angular2Process extends PlayRunHook {
private var watchProcess: Option[Process] = None
private def runProcessSync(command: String) = {
log.info(s"Running $command...")
val rc = Process(command, base).!
if (rc != 0) {
throw new Angular2Exception(s"$command failed with $rc")
}
}
override def beforeStarted(): Unit = {
val cacheFile = target / "package-json-last-modified"
val cacheLastModified = if (cacheFile.exists()) {
try {
IO.read(cacheFile).trim.toLong
} catch {
case _: NumberFormatException => 0l
}
}
val lastModified = (base / "package.json").lastModified()
// Check if package.json has changed since we last ran this
if (cacheLastModified != lastModified) {
runProcessSync("npm install")
IO.write(cacheFile, lastModified.toString)
}
}
override def afterStarted(addr: InetSocketAddress): Unit = {
watchProcess = Some(Process(s"npm run-script ng build -- --watch --delete-output-path=false", base).run)
}
override def afterStopped(): Unit = {
watchProcess.foreach(_.destroy())
watchProcess = None
}
}
Angular2Process
}
}
class Angular2Exception(message: String) extends RuntimeException(message) with FeedbackProvidedException
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment