skip scalajs-bundler, use npm/webpack directly with sbt/scalajs

skip scalajs-bundler, use npm/webpack directly with sbt/scalajs

Many people are using scalajs-bundler to specify their javascript dependencies using npm-style (sbt first, npm second) declarations.

You don’t need to use scalajs-bundler, which can be confusing to use or perhaps less useful in complex bundling scenarios. If all you need is some simpler webpack config, use scalajs-bundler and ignore the content below. If not, you can still retain the flexibility of running npm after your compile triggers on source code change using the standard approach shown below.

First, set up a webpack config. Only the relevant parts are shown:

// config file myapp.webpack.config.js
const common = {
...whatever you want....

}
// I typically parameterize the scala.js output based on the
// build kind but you can also be explicit if you wish
const prod = {
    mode: "production", // prod by default, webpack 4
    entry: path.resolve(__dirname, "target/scala-2.12/me-opt.js"),
    plugins: [
        new webpack.DefinePlugin({
            "proces.env": {
                "NODE_ENV": '"production"'
            }
        }),
        new UglifyJsPlugin({
            parallel: 4,
            cache: true,
            sourceMap: "inline",
        })
    ]
}

const dev = {
    mode: "development", // webpack 4
    entry: path.resolve(__dirname, "target/scala-2.12/me-fastopt.js")
}

// or parameterize your config
/*
function makeConfig(scalapath) {
...
  entry: path.resolve(__dirname, scalapath)
}
const prod = {...}
const dev = {...}
*/

// Use the "name" of the script as the lifecycle
// I usually define a BUILD_KIND flag that is explicit
// and independent of the npm_lifecycle_event value
// (= npm script name)
switch (process.env.npm_lifecycle_event) {
case 'myapp:dev':
    config = merge(common, dev)
    break
    
case 'myapp':
    config = merge(common, prod)
    break

default:
    console.log("No npm_lifecyle_event specified. Using dev config")
    config = merge(common, dev)
    break
}
// I actually usually do the combining of config's in this function
// instead of like the case statement above, but tastes vary.
module.exports = function(env) {
    // do more env based swizzling
    // ...
    return merge(config, ...more config objects...)
}

then in your sbt (1.0+), setup a task that runs after your triggered compile

import scala.sys.process._

val npmBuild = taskKey[Unit]("fullOptJS then webpack")
npmBuild := {
  (fullOptJS in Compile).value
  "npm run myapp" !;
}

val npmBuildFast = taskKey[Unit]("fastOptJS then webpack")
npmBuildFast := {
  (fastOptJS in Compile).value
  "npm run myapp:dev" !
}
// if the code is in a subproject, use (fastOptJS in (subproject, Compile)).value

In sbt, just run npmBuild instead of fastOptJS. Incremental compilation and build of your javascript program will just work. You can use ~npmBuildFast to watch scala sources and run the build during dev.

In package.json, setup some aliases:

"scripts": {
      "build": "sbt npmBuild",
      "dev": "sbt npmBuildFast",
      "myapp": "webpack --progress --config myapp.webpack.config.js",
      "myapp:dev": "webpack --progress --config myapp.webpack.config.js",
      "myappfast:dev:watch": "webpack --progress --config myapp.webpack.config.js --watch",
      "clean": "sbt clean"
  },    

Now you can run on the npm side, scripts that call sbt:

  • npm run build to do a production bulid or
  • npm run dev to run a full build.

On the sbt side, we can run sbt which then automatically runs npm:

  • sbt npmBuild does a full production build
  • sbt npmBuildFast does a fast build.

If we just want to rebundle, perhaps because of changes in the javascript code:

  • npm run myapp: Run bundling for production, with uglify.
  • npm run myapp:dev: Run dev bundling, no uglify, etc.
  • npm run myapp:dev:watch: Watch javascript code for changes, assumes that the scala.js is not changing. Instead of “watch” you can also add a “start” to start a hot reload server as needed i.e. webpack-dev-server instead of webpack.

Obviously, don’t forget to install your dependencies. For example npm i --save office-ui-fabric-react to install the Microsoft fabric UI library. This will add javascript/npm “dependencies” and make them available when you bundle. Of course, this is all standard javascript bundling activity.

If you get webpack bundling warnings about missing sources due to scala.js’s output containing references to “https://githubuser…” and you are using source-map-loader, do

  {
     test: /\.js$/,
     use:["source-map-loader"],
     enforce: "pre",
     exclude: [/node_modules/, scalapathtojs]
  }

for your loaders. Replace scalapathtojs with the path to your scala.js output. That’s why I also make my config a function, so I can pass in the scala.js output path.

As a separate note, if you need a loader that properly loads the sourcemaps output scala.js, use https://github.com/aappddeevv/scalajs-friendly-source-map-loader. It will properly translate “file://” and “http[s]://” source values and retrieve the appropriate source content.

Popular posts from this blog

React, Redux, Recompose and some simple steps to remove "some" boilerplate and improve reuse

cats/cats-effect and webservices function composition

typescript ambient declarations, global.d.ts, lib and typeRoot.md