scalajs-bundler

Cookbook

How to use a custom webpack configuration file?

First, configure the webpackConfigFile setting to refer to your configuration file:

null := Some(baseDirectory.value / "my.custom.webpack.config.js")

Or, if you want to use the same configuration file for both fastOptJS and fullOptJS:

webpackConfigFile := Some(baseDirectory.value / "my.custom.webpack.config.js")

Then, you can write your configuration in file my.custom.webpack.config.js. We recommend that you reuse the configuration file generated by scalajs-bundler and extend it, rather than writing a configuration file from scratch.

You can do so as follows (in file `my.custom.webpack.config.js``):

var webpack = require('webpack');

module.exports = require('./scalajs.webpack.config');

// And then modify `module.exports` to extend the configuration

The key part is the require('./scalajs.webpack.config'). It loads the configuration file generated by scalajs-bundler so that you can tweak it. It works because your configuration file will be copied into the internal target directory, where the scalajs-bundler generates its configuration file, and where all the npm dependencies have been downloaded (so you can also require these dependencies).

By default webpack task only actually launches webpack if it detects changes in settings or in the custom webpack config file. Depending on your usage scenario, you might want to monitor some other files as well (for example, if your webpack config references some additional resources). This can be achieved by using webpackMonitoredDirectories setting:

webpackMonitoredDirectories += baseDirectory.value / "my-scss"
includeFilter in webpackMonitoredFiles := "*.scss"

More fine-grained control over the list of monitored files is possible by overriding the webpackMonitoredFiles task.

You can find a working example of custom configuration file here.

It is also possible to configure a webpack config file to be used in reload workflow and when running the tests. This configuration may not contain entry and output configuration but can be used to configure loaders etc.

These configuration files are configured using webpackConfigFile in reloadTask or webpackConfigFile in Test. For example:

webpackConfigFile in webpackReload := Some(baseDirectory.value / "common.webpack.config.js")

webpackConfigFile in Test := Some(baseDirectory.value / "common.webpack.config.js")

Sharing webpack configuration among configuration files

In addition to the configured webpack config file, all .js files in the project base directory (as configured using the webpackResources setting) are copied to the target directory so they can be imported from the various configuration files.

Here are the steps to share the loader configuration among your prod and dev config files. This uses webpack-merge for convenience. The same result could be accomplished using plain js only.

  1. Put configuration in a common.webpack.config.js file:
module.exports = {
  module: {
    loaders: [
        ...
    ],
    rules: [
        ...
    ]
  }
}
  1. Add webpack-merge to your npmDevDependencies:
npmDevDependencies in Compile += "webpack-merge" -> "4.1.0"
  1. Merge in the common configuration in your dev.webpack.js file:
var merge = require("webpack-merge")
var commonConfig = require("./common.webpack.config.js")

module.exports = merge(commonConfig, {
    ...
})

You can find a working example of a project using a shared configuration file here.

How to use npm modules from Scala code?

Once you have added npm dependencies to the packages you are interested in, you have to import them from your code to effectively use them.

The recommended way to do that is to:

  1. Write a Scala.js facade annotated with @JSImport ;
  2. Refer to this facade from your code.

Let’s illustrate this with an example. Say that you want to write a facade for the following npm module:

exports.bar = function (i) { return i + 1 };
export const bar = i => i + 1;

The corresponding Scala.js facade looks like the following:

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport

@JSImport("foo", JSImport.Namespace)
@js.native
object foo extends js.Object {
  def bar(i: Int): Int = js.native
}

There are several points worth highlighting:

  • The first parameter of the @JSImport annotation is the npm module path. This is the value you would pass to the Nodejs require function ;
  • The second parameter of @JSImport is the name of the imported member, or like in our case, JSImport.Namespace, to import the whole module instead of just one particular member ;
  • The facade is concrete. It can either be a Scala object or a class ;
  • The facade has a “JS native” type.

Other styles of facades (importing a member in particular, importing functions and classes, importing local JavaScript files, etc.) can be found in these tests.

Finally, in your Scala code, just refer to the foo object:

object Main {
  def main(args: Array[String]): Unit = {
    println(foo.bar(42))
  }
}

How to publish a facade for an npm module?

Create a project for the facade and enable the ScalaJSBundlerPlugin as described here.

Implement the facade as explained in the above section.

Publish the Scala.js project as usual.

Finally, to use the facade from another Scala.js project, this one needs both to add a dependency on the facade and to enable the ScalaJSBundlerPlugin plugin.

Projects that use the facade also have to enable the ScalaJSBundlerPlugin plugin, otherwise the dependencies of the facade will not be resolved.

How to use an existing facade assuming the JS library to be exposed to the global namespace?

Webpack is able to require external modules by using imports-loader and expose them to the global namespace by using expose-loader. Thus, you can write a custom webpack configuration file that uses this loaders to expose the required modules to the global namespace. Typically, this file will look like this:

var globalModules = {
  moment: "moment"
};

const importRule = {
  // Force require global modules
  test: /.*-(fast|full)opt\.js$/,
  loader:
    "imports-loader",
  options: {
    type: 'commonjs',
    imports: Object.keys(globalModules)
      .map(function(modName) {
        return {
          moduleName: globalModules[modName],
          name: modName,
        }
      })
  }
};

const exposeRules = Object.keys(globalModules).map(function(modName) {
  // Expose global modules
  return {
    test: require.resolve(modName),
    loader: "expose-loader",
    options: {
      exposes: [globalModules[modName]],
    },
  };
});

const allRules = exposeRules.concat(importRule);

module.exports = {
  performance: { hints: false },
  module: {
    rules: allRules
  }
};

Also, tweak your build.sbt to add the corresponding NPM dependencies and to use the custom webpack configuration file:

npmDependencies in Compile ++= Seq(
  "moment" -> "2.29.1"
)

npmDevDependencies in Compile ++= Seq(
  "webpack-merge" -> "5.7.3",
  "imports-loader" -> "2.0.0",
  "expose-loader" -> "2.0.0"
)

webpackConfigFile in fastOptJS := Some(baseDirectory.value / "dev.webpack.config.js")

webpackConfigFile in Test := Some(baseDirectory.value / "test.webpack.config.js")

// Execute the tests in browser-like environment
requireJsDomEnv in Test := true

You can find a fully working example here.

How to bundle an application having several entry points as exports?

By default, ScalaJSBundlerPlugin assumes that your application only has a main class, activated through scalaJSUseMainModuleInitializer := true, and disregards top-level exports. If you have exports that need to be exposed as several entry points, this will not work.

In such a case, you can use BundlingMode.LibraryAndApplication().

build.sbt:

webpackBundlingMode := BundlingMode.LibraryAndApplication()

Then, assuming that you defined the following library:

package example

import scala.scalajs.js.annotation.{JSExportTopLevel, JSExportAll}

@JSExportTopLevel(name="sjs_example_Library") @JSExportAll
object Library {
  def foo(): String = SomeOtherCode.quux(true)
  def bar(): String = SomeOtherCode.quux(false)
}

You can call its methods as follows from your JavaScript code:

console.log(sjs_example_Library.foo());
console.log(sjs_example_Library.bar());

How to improve the performance of the bundling process?

You can enable the library-only bundling mode and disable source maps:

webpackBundlingMode := BundlingMode.LibraryOnly()
emitSourceMaps := false

How to select specific files from the BundlingMode.Library output

In library-only bundling mode and library with application bundling mode, the webpack task produces multiple files. In order to determine which of these files is, for instance, the BundlerFileType.Application, you can use the _.metadata property of the files, like this:

val files = (webpack in (Compile, fullOptJS)).value
val bundleFile = files
  .find(_.metadata.get(BundlerFileTypeAttr).exists(_ == BundlerFileType.ApplicationBundle))
  .get.data

How to rebuild and reload your page on code changes?

scalajs-bundler includes a simple wrapper over webpack-dev-server to simplify your workflow. It is exposed as two stage-level tasks (startWebpackDevServer and stopWebpackDevServer). The standard work session looks like this:

  1. Spawn background server process:
    > fastOptJS::startWebpackDevServer
    
    By default the server is started on port 8080. Use webpackDevServerPort setting to change this.
  2. Instruct SBT to rebuild on source changes:
    > ~fastOptJS
    
  3. Now each time you change a source file, Scala.js recompiles it, and webpack-dev-server switches to the updated version.
  4. Shut down the background process:
    > fastOptJS::stopWebpackDevServer
    

Additional arguments can be passed to webpack-dev-server via webpackDevServerExtraArgs setting. For example, you can add the following to your build.sbt to make your page reload on every change:

webpackDevServerExtraArgs := Seq("--inline")

How to pass extra parameters to webpack

scalajs-bundler invokes webpack with a configuration generated either automatically from the build or set with webpackConfigFile. webpack is then called with the following arguments:

--config <configfile>

You can add extra params to the webpack call, for example, to increase debugging

webpackExtraArgs := Seq("--profile", "--progress", "true")

Note Params are passed verbatim, they are not sanitized and could produce errors when passed to webpack. In particular, don't attempt to override the --config param.

How to use webpack 4

scalajs-bundler (version 0.12.0 onwards) supports webpack 4. To enable webpack 4, set the correct versions in build.sbt

version in webpack := "4.8.1"

version in startWebpackDevServer := "3.1.4"

Additionally, you need to update any webpack plugins your config uses, to Webpack 4 compatible versions.

Webpack 4 has the potential to substantially reduce your webpack compilation times (80% reductions have been observed but your mileage may vary)

How to get and use a list of assets

scalajs-bundler (version 0.13.0 onwards) will export a list of all assets produced by webpack. You can read that list on sbt

val files = (webpack in (Compile, fullOptJS)).value

You can this list e.g. with sbt-native-packager` to add mappings as:

// Use ScalaJs and the sbt native packager
enablePlugins(ScalaJSBundlerPlugin, UniversalPlugin, UniversalDeployPlugin)

// All files shall go directly into the archive rather than having a top level directory matching
// the module name
topLevelDirectory := None

// Map all assets produced by the ScalaJs Bundler to their location within the archive
mappings.in(Universal) ++= webpack.in(Compile, fullOptJS).value.map { f =>
  f.data -> s"assets/${f.data.getName()}"
}

This will add all artifacts produced by the fully optimized Scala.JS run to the 'assets' directory of the target archive.

If you need to package additional libraries that have been downloaded by scalajs-bundler, you can do something like:

// Add any other required files to the archive
mappings.in(Universal) ++= Seq(
  target.value / ("scala-" + scalaBinaryVersion.value) / "scalajs-bundler" / "main" / "node_modules" / "react" / "umd" / "react.production.min.js" -> "assets/react.production.min.js",
  target.value / ("scala-" + scalaBinaryVersion.value) / "scalajs-bundler" / "main" / "node_modules" / "react-dom" / "umd" / "react-dom.production.min.js" -> "assets/react-dom.production.min.js"
)

Also, any static resources that you would like to have in the resulting archive (i.e. index.html), should live inside the src/universal directory of your project.