Search code examples
twitter-bootstrapscalajs-bundler

How to include bootstrap with scalajs-bundler


I am using the scalajs-bundler plugin and have defined my build.sbt thus:

enablePlugins(ScalaJSBundlerPlugin)

name := "Reproduce"
scalaVersion := "2.12.8"

npmDependencies in Compile += "bootstrap" -> "3.4.1"

However when I run "sbt fastOptJS::webpack" there is no reference to bootstrap in the -fastopt-bundle.js file that gets generated.

Shouldn't bootstrap get included?


Solution

  • I just ran into the exact same issue and was not able to find the solution in any searches. Hopefully this (rather long) answer will help others avoid some pain.

    I think there are 3 issues to address in order to use the Bootstrap lib bundled as an npm module (ie. using scalajs-bundler to package it via the npmDependencies parameter).

    1) Get the bootstrap library into the bundle.
    2) Make the jQuery symbol available to Bootstrap as a global variable.
    3) Get Bootstrap loaded at run time.

    1) Getting modules into the bundle

    This is first issue that you mention in your question. Adding bootstrap to npmDependencies is not sufficient to get scalajs-bundler to include your module in the bundle. In addition to that, somewhere in your scala code you must have a JSImport("library_name", ...) statement. This tells scalajs-bundler that you are actually using the library and that it needs to be included in the bundle. You can read more detail about JSImport here. I found that description a little vague. Here is an answer to a question by me from @Julien Richard-Foy that I found much more helpful. In my code the JSImport requirement is met included in part 3 below. Note, you will also need to have a JSImport for the jquery lib to make sure it gets included in the bundle also since Bootstrap depends on it, and you need to add jquery to npmDependencies.

    2) Creating javascript global variables

    This is the most complicated part of the solution.

    In my application I would get an error in the browser console like jQuery is not defined. It took me some time to determine this was caused inside the Bootstrap lib. The Bootstrap lib depends on the jquery lib by assuming the global variable jQuery is defined. Unfortunately, simply including jquery in the bundle via npmDependencies and JSImport are not enough. You must tell scalajs-bundler to create the jQuery global var and export it to all the modules in your bundle.

    The solution @Julien Richard-Foy points to is the general recipe to follow, but I believe it has a bug. The bug doesn't cause a problem in their example because the library name and the global variable name are identical. The problem is that modName and globalModules[modName] are swapped on the return line of the importRule.

    Here is my common.webpack.config.js file for scalajs-bundler instructing it to generate a global variable jQuery and export it. Note, the only changes I made were to place jquery: "jQuery" in globalModules and to swap positions of modName and globalModules[modName] on the return line of the importRule. Otherwise, I just followed the example (ie. other config files and changes to build.sbt are also required).

    var globalModules = {
      jquery: "jQuery"
    };
    
    const importRule = {
      // Force require global modules
      test: /.*-(fast|full)opt\.js$/,
      loader:
        "imports-loader?" +
        Object.keys(globalModules)
          .map(function(modName) {
            return globalModules[modName] + "=" + modName;
          })
          .join(",")
    };
    
    const exposeRules = Object.keys(globalModules).map(function(modName) {
      // Expose global modules
      return {
        test: require.resolve(modName),
        loader: "expose-loader?" + globalModules[modName]
      };
    });
    
    const allRules = exposeRules.concat(importRule);
    
    module.exports = {
      performance: { hints: false },
      module: {
        rules: allRules
      }
    };
    

    Some additional resources on the operation of the imports-loader and expose-loader.

    3) Loading modules at run time

    Normally this is not something you have to do explicitly. However, in the case of Bootstrap (or any js library that extends another js library) you probably won't be calling the library directly through a facade. Most likely you will be using the Monkey Patching pattern. In this case a jQuery object is cast to a Bootstrap object without directly invoking the Bootstrap lib. Typically this will compile just fine, but you will get a run time error in the browser console, something like Uncaught TypeError: jq.modal is not a function. Here is how the Bootstrap extension to jquery is defined so you can make some sense of jq and modal in the error message:

      @js.native
      trait BootstrapJQuery extends JQuery {
        def modal(action: String): BootstrapJQuery = js.native
        def modal(options: js.Any): BootstrapJQuery = js.native
      }
    
      implicit def jq2bootstrap(jq: JQuery): BootstrapJQuery = jq.asInstanceOf[BootstrapJQuery]
    

    The solution is to make some explicit reference to the lib sometime before the implicit is invoked.

    Here's how I did it.

      private object BootstrapLib {
        @js.native
        @JSImport("bootstrap", Namespace)
        object BootstrapModule extends js.Object
    
        private lazy val dummy = BootstrapModule
    
        def load() = dummy
      }
      BootstrapLib.load()
    

    This is contained inside an object that contains all the wrapper definitions for Bootstrap components. This guarantees the Bootstrap.load() gets called before any of the Bootstrap wrappers are used. I like this since there is no need to remember to call it explicitly in any of the wrapper factory methods.