Monday, October 7, 2019

Create NodeJS based application using Docker WebPack and Babel

Javascript is a simple language, and using it makes new applications development very fast.
However, unlike other languages, such as Java, which can include maven as it build system, Javascript does not include a easy and fast build system. You will need to invest time in creation of the build and tests.

In this article we lay the foundations of creating a new NodeJS application that is using Webpack and babel for it transpilation (see this link for transpilation details). In addition, we will pack the application using a docker image, so it will be ready for deployment.


  


Let's start with folder structure. We will later explain and present the content for each file.
  • demo-application
    • Dockerfile
    • Dockerfile.test
    • .dockerignore
    • src
      • package.json
      • package-lock.json
      • .babelrc
      • webpack.config.js
      • main
        • demo-main.js
      • test
        • demo-main.test.js

demo-main.js

This is the actual application code.
In this example, we connect to MongoDB using mongoos library.

const mongoose = require('mongoose');
console.log('connecting to MongoDB');
mongoose.connect('mongodb://127.0.0.1:27017/db', {useNewUrlParser: true})
  .then(()=>{
     console.log('connected!');
   });

demo-main.test.js

This is where we run our unit tests.
We use proxyquire to import our code, and replacing any dependencies with our own stub.
This ensures that we are running unit test without any external dependencies.
In this case, we replace the mongoos dependency with a stub.

const chai = require('chai');
const chaiHttp = require('chai-http');
const proxyquire = require('proxyquire');
const expect = chai.expect;
chai.should();
chai.use(chaiHttp);


async function importCode() {
  proxyquire.noCallThru().load('../main/demo-main.js',
    {
      'mongoose': {
        connect: function(){
   console.log('stub connection to mongo');
 }
      }
    }
  );
}

describe('tests', () => {
  it('check main code', async () => {
    const main = await importDeploy();
  });
});

package.json

The package json includes the dependencies, as well as the build and test commands.
The scripts include:

  • build: creates bundle.js from our source
  • start: run the application
  • dev: runs nodemon which automatically recompiles the application upon source change. This is useful for debugging
  • unit-test: run the tests, and create coverage report
The dependencies include the only dependency we have in our code: mongoose.

The dev dependencies include all the libraries that we need for transpilation, and for unit test.


{
  "name": "demo",
  "scripts": {
    "build": "webpack --config ./webpack.hook.js",
    "start": "node build/bundle.js",
    "dev": "nodemon --watch main --exec node build/bundle.js",
    "unit-test": "nyc mocha ./test/**/*.js 
                  --require @babel/register 
                  --require @babel/polyfill 
                  --timeout 10000 
                  --reporter=xunit 
                  --reporter-options output=coverage.xml"
  },
  "dependencies": {
    "mongoose": "^5.6.9"
  },
  "devDependencies": {
    "@babel/cli": "^7.5.5",
    "@babel/core": "^7.5.5",
    "@babel/node": "^7.5.5",
    "@babel/plugin-proposal-class-properties": "^7.5.5",
    "@babel/polyfill": "^7.4.4",
    "@babel/preset-env": "^7.5.5",
    "@babel/register": "^7.5.5",
    "babel-loader": "^8.0.6",
    "chai": "^4.2.0",
    "chai-http": "^4.2.1",
    "mocha": "^6.0.2",
    "nodemon": "^1.18.10",
    "nyc": "^14.1.1",
    "proxyquire": "^2.1.0",
    "webpack": "^4.39.2",
    "webpack-cli": "^3.3.6"
  },
  "nyc": {
    "all": true,
    "reporter": [
      "lcov"
    ]
  }
}

package-lock.json

This file is automatically generated once we run `npm install`.
The file includes all the libraries that the project depend on.
This file should be added to the source control system, to ensure stability of the project.

.babelrc

This file instruct the babel to automatically select the transpilation level that we need.
In addition, we include the source maps in the bundle.js, so we will be able to debug our code.
{
  "presets": [
    "@babel/preset-env"
  ],
  "sourceMaps": true,
  "retainLines": true
}

webpack.config.js

This file configured webpack.
It instruct webpack to create the bundle.js file, and set the demo-main.js as the starting point for the project.

const path = require('path');
const nodeExternals = require('webpack-node-externals');
const env = process.env.NODE_ENV || "development";

console.log(`node environment is ${env}`);
console.log(`current dir is ${process.cwd()}`);

module.exports = {
    mode: env,
    target: 'node',
    entry: [
        '@babel/polyfill',
        './main/demo-main.js'
    ],
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'build'),
    },
    devtool: "eval-source-map",
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: "babel-loader",
                exclude: "/node_modules/",
                options: {
                    presets: ["@babel/preset-env"]
                }
            }
        ]
    },
    externals: [nodeExternals()],
};

Dockerfile

The Dockerfile contains instructions to build the docker image for our application.
Notice that the docker entry point calls directly to node, due to SIGTERM killing npm without waiting for the child node process.
To run the build, run: `docker build -f Dockerfile`

FROM node:12.7 as builder
WORKDIR /app
COPY src/package.json ./
COPY src/package-lock.json ./
RUN npm install
COPY src/ ./
RUN npm run build
CMD ["node", "build/bundle.js"]

Dockerfile.test

Very similar to the one before, only that we run the tests, instead of the application.
To run the tests, run: `docker build -f Dockerfile.test`
FROM node:12.7 as builder
WORKDIR /app
COPY src/package.json ./
COPY src/package-lock.json ./
RUN npm install
COPY src/ ./
RUN npm run build
CMD ["npm","run", "unit-test"]

.dockerignore

This file contains the list of files we want to avoid from being sent to the docker service upon build.
This saves significant time upon build.
**/node_modules
**/build


No comments:

Post a Comment