bowlingx / play-webpack

play module to support webpack and server side rendering in a simple way

GitHub

play-webpack

Codacy grade CircleCI Maven Central

This play module will add support for webpack and server-side rendering of javascript with the built-in nashorn script engine.

Example Project: https://github.com/BowlingX/play-webpack-example

Requirements

  • JDK 8
  • scala 2.11 or scala 2.12
  • play 2.6
  • webpack or anything the generates a JSON file like the one below

Setup

Create a file in ~/project/play-webpack.sbt

addSbtPlugin("com.bowlingx" %% "play-webpack-plugin" % "0.1.19")

Add the following dependencies:

libraryDependencies += "com.bowlingx" %% "play-webpack" % "0.1.19"

The plugin will convert a webpack JSON manifest file (generated with https://github.com/kossnocorp/assets-webpack-plugin) to a scala object that can be used directly in play templates for example. The plugin is theoretically not limited to play. I will extend the project to support other frameworks in the future.

Since version 0.17.0 the plugin supports a plain json format (like https://github.com/webdeveric/webpack-assets-manifest).

To make the assets available in your template file:

TwirlKeys.templateImports += "com.bowlingx.webpack.WebpackManifest"

Sample JSON File (generated by assets-webpack):

{
  "vendor": {
    "js": "/assets/compiled/vendor.js",
    "css": "/assets/compiled/vendor.css"
  },
  "server": {
    "js": "/assets/compiled/server.js"
  },
  "main": {
    "js": "/assets/compiled/main.js",
    "css": "/assets/compiled/main.css"
  },
  "manifest": {
    "js": "/assets/compiled/manifest.js"
  }
}

Sample twirl Template:

@(title: String)(content: Html)
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>@title</title>
        @WebpackManifest.main.css.map { file =>
            <link rel="stylesheet" media="screen" href="@file">
        }
        <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
    </head>
    <body>
        @content
        @Seq(WebpackManifest.manifest.left.js, WebpackManifest.vendor.left.js, WebpackManifest.main.left.js).flatten.map { file =>
            <script src="@file" type="text/javascript"></script>
        }
    </body>
</html>

Default Settings

The plugin defines the following configuration (your application.conf):

webpack {
  # the default bundles to include
  prependBundles = ["manifest", "vendor"]

  # The object that is generated by the plugin (needs to implement `WebpackManifestType`)
  manifestClass = "com.bowlingx.webpack.WebpackManifest$"

  # The public path of the assets
  publicPath = "/assets/compiled"
  # The path where they are stored relative to project root
  serverPath = "/public/compiled"
  
  rendering {
      forceDisableWatch = false
      timeout = 1minute
      renderers {
        prod = 5
        dev = 1
        test = 1
      }
    }
}

The default path of the manifest file (relative to project root) (in your build.sbt)

webpackManifest := file("conf/webpack-assets.json").some.filter(_.exists).toSeq

You can supply multiple files that will then be merged.

In case you need to prefix the assets, you can do that with (defaults to None):

webpackAssetPrefix := Some("/prefix/")

Server-Side Rendering with Nashorn

The main purpose of this plugin is to provide an easy way to render javascript inside a play action. After enabling the module in you application.conf, you can hook up your controller like this:

@Singleton
class YourPlayController @Inject()(engine: Engine, components: ControllerComponents)(implicit context:ExecutionContext) extends AbstractController(components) {

  def index: Action[AnyContent] = Action.async {
    engine.render("yourGlobalMethod", "any", "list", "of", "arguments") map {
      case Success(Some(renderResult)) => Ok(renderResult.toString)
      case _ => NotFound
    }
  }
}

See a full example in src/play-module/src/test.

Important: Prevent modifying the global state of the JavaScript environment. Due to performance reasons, contexts are reused and any changes are persisted inside the engine if you do so. To prevent memory leaks keep your methods pure.

You can configure the number of rendering actors in webpack.rendering.renderers:

rendering {
    forceDisableWatch = false
    timeout = 1minute
    renderers {
      prod = 5
      dev = 1
      test = 1
    }
  }

The more renderers you setup, the more requests you can handle. It defaults to 1 in tests and dev to speed up the init process.

Promises

The library makes it possible to use an async result. Support for setTimeout and clearTimeout exists since version 0.1.5.

All this just requires to return a Promise in the render function. A simulated Promise looks like this:

global.yourGlobalMethod = function () {
  return {
    then: function (resolve, reject) {
      setTimeout(function () {
        resolve("This is an async resolved String");
      }, 100);
    }
  };
};

If you need Promise support in your library, use any polyfill available. This library does not ship with a polyfill.

Supported Libraries (tested by the Author)

Workflow

Any file changes in dev mode (including the manifest file) are picked up automatically and a recompilation is triggered, so the normal "change and reload" cycle that leads to a faster development experience is kept.

You can force to disable watch mode in dev mode by setting webpack.rendering.forceDisableWatch to true.