Minify and Lint JavaScript with npm and Docker

Today's JavaScript minification and linting is popularly done with node.js packages. I want to run a couple of these packages on some JavaScript files, but I don't want to install npm on my machine.

Docker is perfectly suited for this task.

This article assumes some knowledge of Docker, and that you have it installed already. I'm running Docker here on a linux machine, where I have access to bash shell scripting as well.

Create a folder somewhere. I'm calling this image min-lint. Here's a look at what this folder will contain:

$ ls -1 min-lint-docker/
Dockerfile
js-input-files/
js-output-files/
run-min-lint.sh
run.sh

For linting I am installing jshint, and for minifying, terser. Let's start with the script that will actually run them. The script will need two arguments, the input and output folders. Here's run-min-lint.sh:

#!/bin/bash
#
# run jshint on the entire input dir (arg 1)
#   (results written to stdout)
#
# run terser to minify each .js file individually
#   found in input directory (arg 1)
#
# output to a file of the same name in the given
#   output directory (arg 2)
#

jshint --config "${1}"/jshint-config.json "${1}"

find "${1}" -type f -name "*.js" | while read JS_FILE
do
  terser "${JS_FILE}" --output "${2}/`basename ${JS_FILE}`"
done

Next, define a small Docker image that installs the npm executables, and runs the above script. Here's the Dockerfile:

# use the small alpine-based node.js image
FROM node:17-alpine

# I like to use bash scripts, so install that
RUN apk add --no-cache bash

# install with -g flag so the terser and jshint executables can be used by the shell
RUN npm install terser -g
RUN npm install jshint -g

# trailing slash is important, to copy file into the dir
COPY run-min-lint.sh /opt/

# these dirs are bind mounted from the host when this
#   container image is run
ENTRYPOINT ["/opt/run-min-lint.sh", "/js-input-files/", "/js-output-files/"]

To run the image, we need to run a few commands, so I'm using a bash script. Docker’s "bind mounted" volumes allow us to access a pair of directories both on the host and within the container. Here's the run.sh:

#!/bin/bash

# ensure we can run this script from anywhere on the host
THIS_SCRIPT="`readlink -f "${0}"`"
THIS_DIR="`dirname "${THIS_SCRIPT}"`"
cd "${THIS_DIR}"

mkdir -p js-input-files
mkdir -p js-output-files

# remove any previous dangling images, if any
OLD_IMAGES="$(docker images --filter "label=me.philthompson.image=min-lint" --filter "dangling=true" -q --no-trunc)"
if [ ! -z "${OLD_IMAGES}" ]
then
  docker image rm $OLD_IMAGES
fi

# build a docker image using the Dockerfile in this directory
docker build --label me.philthompson.image=min-lint -t min-lint .

# run the image (NOT in -d detached mode)
# for -v, the host side of the "host:container" syntax
#   must be either an absolute path, or a volume name
# make sure to include "--rm" so Docker cleans up afterwards
docker run \
  --rm \
  -v ${THIS_DIR}/js-output-files:/js-output-files \
  -v ${THIS_DIR}/js-input-files:/js-input-files \
  min-lint

That's about it! Put the JavaScript files (and jshint-config.json file) into the js-input-files/ directory, run the image with run.sh, and voilà. The minified files are written to the js-output-files/ directory. Linting messages are visible in the stdout stream of run.sh.