unbundled #1

This is the simplest CLI model to implement. Our "bundled1" approach keeps all application authored content in the "src" folder and the application is bundled into a single output file using a webpack first build model.

Here are the main ingredients:

  • .scala files to bundled together using sbt

  • Other application .js source files, all of which will be bundled together.

  • External dependencies from npm (nodejs based) NOT bundled into our application.

scala

  • src/main/scala/app:

    • cli.app - main cli app that prints out the command line parameters and contains some small facades to access chalk and table

We use the npm versions of chalk and table.

Dependencies

  • scala:

    • scopt (scalajs version)

  • pure javascript:

    • src/main/resources/cli/ContentFormatting.js

    • src/main/resources/formatting.js

    • src/main/scala/app/messages.js

  • facades:

    • io.scalajs.nodejs - comprehensive nodejs facade

  • npm (left unbundled):

    • chalk - colorized terminal output

    • table - table output

Outputs

  • bin:

    • cli.js - output cli executable (shell script)

    • cli.js.map - source map

  • node_modules

    • chalk

    • table

Our core output cli.js will be built using webpack. webpack will take both the output of scalajs as well as the dependency*.js files and bundle them into a single output file.

Build config

This example requires both sbt and webpack to create the final outputs in the form we want. We could use sbt for some of the last processing steps but that would require some code and knowing more about sbt vs webpack configuration. Granted, both sbt programming and webpack configuration are complex topics, but we want to show how webpack fits into the overall build especially when there are alot of moving parts.

In the end, the build will be a webpack led build cycle.

Here's our cli.webpack.config.js:

There's alot here so lets take it in pieces.

// // Based on lifecycle, optimize some outputs. // webpack -p already adds webkpace.DefinePlugin(...NODE_ENV="production") // so we do not add it here. Instead of uglify we could also use GCC // but the core scala has already gone through uglify if fullOptJS was used. //

Imports

var webpack = require('webpack')
var nodeExternals = require('webpack-node-externals')
var fs = require("fs")

Since the webpack config file is a nodejs module, we can require what we need at the top.

Specify start of webpack graph

entry: './src/main/scala/app/cli.scala',
output: { filename: 'index.js' },
target: "node",
externals: [nodeExternals()],

This specifies that webpack should assume the cli.scala file is the entry point for our CLI program. The output will be an index.js file in the root directory. Our target build is "node" which tells webpack not to bundle in nodejs standard libraries like "os" or "fs".

However, we need to specify the loader that translates .scala files into nodejs artifacts in the webpack graph.

Webpack loaders

module: {
rules: [
{
test: /\.scala$/,
loader: 'scalajs-loader',
options: {
jsStage: 'fullOptJS',
dirSegment: 'scalajs-bundler/main'
}
}
]
},

This is the loader for .scala files. The scalajs-loader npm module provides a webpack loader that shells out to sbt to build. Hence, we need a build.sbt file which is

enablePlugins(ScalaJSPlugin)

name := "unbundled1"
scalaVersion := "2.12.2"
scalaJSModuleKind := ModuleKind.CommonJSModule
scalaJSUseMainModuleInitializer := true

resolvers += Resolver.jcenterRepo

libraryDependencies ++= Seq(
"io.scalajs" %%% "nodejs" % "0.4.0-pre5",
"com.github.scopt" %%% "scopt" % "latest.version"
)

jsDependencies ++= Seq(ProvidedJS / "cli/bundleme.js")

skip in packageJSDependencies := true

Our project/* files are:

sbt.version=0.13.15

and

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.15")

Notice that we only declare one jsDependencies, 'bundleme.js' that should be bundled into the core sbt-scalajs output file. This is for illustration purposes only. You could skip bundling any js files in, and in fact, its probably better to keep then unbundled.

Plugins

plugins: [
new webpack.BannerPlugin({banner:"#!/usr/bin/env node", raw: true}),
function() {
this.plugin('done', () => {
fs.chmodSync("index.js", "755");
fs.renameSync("index.js", "unbundled1");
})
}
]

This last webpack config says to add the shebang line to the output, index.js. Then, an inline webpack module is specified which changes the execution mode and final name of the CLI program.

package.json

We never really specified our package.json file, which we need to do because need to declare our dependencies and ensure that when nodejs installs our CLI it knows how to do that. Also, package.json holds toplevel "scripts" to execute to run the build:

{
"bin": {
"unbundled": "./unbundled"
},
"description": "Scalajs+nodejs CLI application",
"name": "cli-app",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"build": "webpack --progress --config cli.webpack.config.js"
},
"dependencies": {
"chalk": "^1.1.3",
"table": "^4.0.1"
},
"devDependencies": {
"scalajs-loader": "0.0.1",
"webpack": "^2.5.0",
"webpack-node-externals": "^1.5.4"
}
}

The dependencies were declared saved using npm install --save-dev <dependency> or npm install --save <dependency> as appropriate.

Last updated