React development workflow & release mgmt with NPM & Webpack

Who am I ?

Davy De Waele

Developer / Architect @ Ixor

 

@ddewaele

Goal of this talk

  • Present a pragmatic development workflow
  • Define the role of npm & webpack in that flow
  • Focus on release management / devops
  • Give you a complete end-to-end picture of the process

What do we want

  • Short feedback cycles while coding
  • Ability to run automated tests
  • Do continuous integration
  • Version / release and publish our code
  • Automated the process as much as possible

NPM

Node Package Manager

Webpack

What tools do we need ?

NPM

NPM

  • What is NPM
  • What does it try to solve
  • How does it do it

What is NPM

  • Default package manager for NodeJS runtime
  • Code distribution system
    • Find code
    • Download code
    • Install code
    • Package code
    • Publish code

Package Manager

What does it host ?

  • Traditionally very backend oriented
    • Frameworks & Libraries 
    • Tools
    • Command line interfaces
  • Recently more frontend-related stuff
    • Web tools (grunt / gulp / browsify / webpack)
    • Web UI frameworks (bootstrap / react / angular)
    • CSS frameworks
    • ....

NPM vs Git

  • Git is where you put all of your source-code.
  • But also your docs / tests / branches / notes / .....
  • It's the stuff you want to collaborate on with your collegues during development.
  • NPM is where you put the stuff you want to share  with your clients.
  • This can be compiled sources / assets, but also code

Title Text

What is in an npm package

  • Source-code
  • Compiled-code
  • Binary asserts
  • Minified / uglified artifacts

It ultimately depends on your use-case and if you are a framework author, a client developer, ...

What does it try to solve ?

Code distribution

Dependency hell

Code Distribution

Finding

code

Downloading
code

Installing code

Packaging code

Releasing code

Publishing code

Finding Code

Finding code

Downloading packages

  • All NPM packages are contained in a registry
  • Every package has a set of meta-data
  • Identified by scope / name / version
  • Can be public / private

Downloading code

npm install react --save

@ixor/testapp1@1.0.1 /Users/ddewaele/Projects/Node/testapp1
└─┬ react@0.14.7 
  ├─┬ envify@3.4.0 
  │ ├─┬ jstransform@10.1.0 
  │ │ ├── base62@0.1.1 
  │ │ ├── esprima-fb@13001.1001.0-dev-harmony-fb 
  │ │ └─┬ source-map@0.1.31 
  │ │   └── amdefine@1.0.0 
  │ └── through@2.3.8 
  └─┬ fbjs@0.6.1 
    ├── core-js@1.2.6 
    ├─┬ loose-envify@1.1.0 
    │ └── js-tokens@1.0.2 
    ├─┬ promise@7.1.1 
    │ └── asap@2.0.3 
    ├── ua-parser-js@0.7.10 
    └── whatwg-fetch@0.9.0 
{
  "name": "@ixor/testapp1",
  "version": "1.0.1",
  "description": "Greatest app in the world",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ddewaele/testapp1.git"
  },
  "keywords": [
    "greatest",
    "app",
    "world"
  ],
  "author": "Davy De Waele <ddewaele@gmail.com> (https://github.com/ddewaele/)",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/ddewaele/testapp1/issues"
  },
  "homepage": "https://github.com/ddewaele/testapp1#readme",
  "dependencies": {
    "bootstrap": "^3.3.6",
    "react": "^0.14.7"
  }
}
{
  "name": "react-build",
  "private": true,
  "version": "0.14.7",
  "devDependencies": {
    "babel": "^5.8.3",
    "babel-eslint": "^4.1.3",
    "benchmark": "^1.0.0",
    "browserify": "^9.0.3",
    "bundle-collapser": "^1.1.1",
    "coffee-script": "^1.8.0",
    "del": "^1.2.0",
    "derequire": "^2.0.0",
    "envify": "^3.0.0",
    "eslint": "^1.5.1",
    "eslint-plugin-react": "^3.4.2",
    "eslint-plugin-react-internal": "file:eslint-rules",
    "fbjs": "^0.6.1",
    "fbjs-scripts": "^0.2.0",
    "grunt": "^0.4.5",
    "grunt-cli": "^0.1.13",
    "grunt-compare-size": "^0.4.0",
    "grunt-contrib-clean": "^0.6.0",
    "grunt-contrib-compress": "^0.13.0",
    "gulp": "^3.9.0",
    "gulp-babel": "^5.1.0",
    "gulp-flatten": "^0.1.0",
    "gulp-util": "^3.0.5",
    "gzip-js": "~0.3.2",
    "jest-cli": "^0.5.7",
    "jstransform": "^11.0.0",
    "object-assign": "^3.0.0",
    "optimist": "^0.6.1",
    "platform": "^1.1.0",
    "run-sequence": "^1.1.0",
    "through2": "^2.0.0",
    "tmp": "~0.0.18",
    "typescript": "~1.4.0",
    "uglify-js": "^2.4.23",
    "uglifyify": "^3.0.1"
  },
  "devEngines": {
    "node": "4.x",
    "npm": "2.x"
  },
  "commonerConfig": {
    "version": 7
  },
  "scripts": {
    "build": "grunt build",
    "linc": "git diff --name-only --diff-filter=ACMRTUB `git merge-base HEAD master` | grep '\\.js$' | xargs eslint --",
    "lint": "grunt lint",
    "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json",
    "test": "jest"
  },
  "jest": {
    "modulePathIgnorePatterns": [
      "/.module-cache/",
      "/react/build/"
    ],
    "persistModuleRegistryBetweenSpecs": true,
    "rootDir": "",
    "scriptPreprocessor": "scripts/jest/preprocessor.js",
    "setupEnvScriptFile": "scripts/jest/environment.js",
    "setupTestFrameworkScriptFile": "scripts/jest/test-framework-setup.js",
    "testFileExtensions": [
      "coffee",
      "js",
      "ts"
    ],
    "testPathDirs": [
      "<rootDir>/eslint-rules",
      "<rootDir>/src",
      "node_modules/fbjs"
    ],
    "unmockedModulePathPatterns": [
      ""
    ]
  }
}

Dependencies

NPM will download the dependencies, including all the transitive dependencies

Local vs Global installs

  • npm install can be executed within your project (in a folder containing a package.json)
  • npm install can be used to install packages globally

Dependency Hell

Semantic Versioning

  • MAJOR version when you make incompatible API changes

 

  • MINOR version when you add functionality in a backwards-compatible manner

 

  • PATCH version when you make backwards-compatible bug fixes.

NPM SemVer

"devDependencies": {
    "btoa": "~1.1.2",
    "glob": "~6.0.1",
    "grunt": "~0.4.5",
    "grunt-sed": "github:twbs/grunt-sed#v0.2.0",
    "markdown-it": "^5.0.0",
    "npm-shrinkwrap": "^200.4.0",
    "time-grunt": "^1.2.1"
  }

Tilde ranges (~1.2.3)

  • Allows patch-level changes if a minor version is specified.
  • Allows minor-level changes if no minor version is specified.

 

 

 

 

Scenario : Flexible patches. Typically used to allow patches

~1.2.3 := >=1.2.3 <1.(2+1).0 := >=1.2.3 <1.3.0
~1.2 := >=1.2.0 <1.(2+1).0 := >=1.2.0 <1.3.0 (Same as 1.2.x)
~1 := >=1.0.0 <(1+1).0.0 := >=1.0.0 <2.0.0 (Same as 1.x)

Caret ranges (^1.2.3)

  • Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch]

 

 

 

Scenario :
^0.2.3 will accept versions up untill a possible breaking change : >=0.2.3 <0.3.0

 

^1.2.3 := >=1.2.3 <2.0.0
^0.2.3 := >=0.2.3 <0.3.0
^0.0.3 := >=0.0.3 <0.0.4

Does this really work ?

  • Surprisingly yes ... well... most of the time
  • Given
    • The number / size of libraries
    • The frequency of new versions being released
    • The dependencies between them
  • It continues to surprise me nothing bad actually happens 
  • It does partially solves the dependency hell issue


but.....

Does this really work ?

The Polymer issue

  • Hot off the press : A new polymer release was broken

 

 

 

"the team discovered a nested npm dev dependency that broke the world after a change in version from 1.1.1 to 1.2.0"

 

But wait ....  that doesn't count as a breaking change. npm happily installed 1.2.0 based on the package's install rule of ^1.0.0. It shouldn't have broken anything, but it did.

Solution : shrinkwrap

Shrinkwrap

{ "name": "client",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Davy De Waele <ddewaele@gmail.com> (https://github.com/ddewaele/)",
  "license": "ISC",
  "dependencies": {
    "@ddewaele/module-a": "^0.1.1"
  }}
{
  "name": "client",
  "version": "1.0.0",
  "dependencies": {
    "@ddewaele/module-a": {
      "version": "0.1.3",
      "from": "@ddewaele/module-a@0.1.3"
    }
  }
}

Module A

0.1.1

Module A

0.1.2

Module A

0.1.3

Client 

Module A

^0.1.1

T1

T2

T3

Module A

0.1.4

T4

Client 

Client 

Client 

Module A

0.1.1

Module A

^0.1.1

Module A

^0.1.1

Module A

^0.1.1

Module A

0.1.2

Module A

0.1.3

Module A

0.1.4

Shrinkwrap

{
  "name": "client",
  "version": "1.0.0",
  "dependencies": {
    "@ddewaele/module-a": {
      "version": "0.1.2",
      "from": "@ddewaele/module-a@0.1.2"
    }
  }
}

npm shrinkwrap, when executed, will look at the current versions of all dependencies, and will store those versions in an npm-shrinkwrap.json file, essentially locking in your dependency versions

Module A

0.1.1

Module A

0.1.2

Module A

0.1.3

Client 

Module A

^0.1.1

T1

T2

T3

Module A

0.1.4

T4

Client 

Client 

Client 

Module A

0.1.1

Module A

^0.1.1

Module A

^0.1.1

Module A

^0.1.1

Module A

0.1.2

Module A

0.1.2

Module A

0.1.2

NPM Dependencies

  • NPM packages allow us to declare dependencies.
  • Works really well for traditional Node components.
  • A bit of a shoe-horn when it comes to webapp dependencies

 

 

Traditonal NPM dependencies

component A

component B

client

  • All dependencies are nicely layed out
  • module-a needs the dependency
    to module-b
//module-a.js
var moduleB = require("@ddewaele/module-b");

module.exports = {
	sayHello: function() {
		var pjson = require('../package.json');
		return "Hello from componentA v" + pjson.version + " and " + moduleB.sayHello()
	}
}
  • The client only needs a dependency to model-a, but gets the transitive dependencies via npm
//client.js
var component = require("@ddewaele/module-a");

console.log(component.sayHello());

What about web dependencies ?

  • Before NPM was gaining traction as a buil tool, people weren't using it for managing web dependencies.
  • They used something called Bower

 

{
  "name": "app-name",
  "version": "0.0.1",
  "dependencies": {
    "sass-bootstrap": "~3.0.0",
    "modernizr": "~2.6.2",
    "jquery": "~1.10.2"
  },
  "private": true
}

Modules vs WebApps

  • NPM is typically used to distribute code for re-usable modules.
  • Library authors create a re-usable module / component and share its code.
  • But what about webapps ? Is a webapp a re-usable module ?
  • No, in most cases they are not.

Webapps packages

So what should we put in our webapp npm package ? 

Well... according to Laurie Voss, CTO of NPM inc : 

NPM Flow

Steps Commands
Creating an app npm init
​Adding dependencies npm install
Locking dependency versions npm shrinkwrap
Testing your app npm test
Packaging your app npm pack
Versioning your app npm version
Publishing your app npm publish
Running scripts npm run-script

NPM Demo

Release Management

What do we need ?

  • A version number
  • A way to tag the code that we want to release
  • A way to build / package a release
  • A way to publish a release
  • An installation / update mechanism
  • A central location where all of this can take place.

What have we got ?

  • A tarbal via npm pack
  • A version number via npm version
  • An install / update system via npm install / update
  • A release mechanism (scm tagging / tarbal versioning) via npm version / npm publish
  • A secure repo (even for private modules) via the npm registry

 

Does this work for SPAs?

  • I believe it does
  • As long as we remember not to see a webapp as a re-usable module
  • We need to think about what we want to release
  • There's no point in releasing our ES6 code, as that has zero run-time value
  • So we only need to release our minified / bundled webapp resources. (our dist folder).

Webpack

Webpack

  • What is Webpack
  • What does it do
  • What does it try to solve
  • How does it do it

What is Webpack

  • Webpack is a module bundler
  • Webpack is not a task runner

History lesson

In the beginning there was

Grunt is a task runner

/*!
 * Bootstrap's Gruntfile
 * http://getbootstrap.com
 * Copyright 2013-2015 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 */

module.exports = function (grunt) {
  'use strict';

  // Force use of Unix newlines
  grunt.util.linefeed = '\n';

  RegExp.quote = function (string) {
    return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
  };

  var fs = require('fs');
  var path = require('path');
  var npmShrinkwrap = require('npm-shrinkwrap');
  var generateGlyphiconsData = require('./grunt/bs-glyphicons-data-generator.js');
  var BsLessdocParser = require('./grunt/bs-lessdoc-parser.js');
  var getLessVarsData = function () {
    var filePath = path.join(__dirname, 'less/variables.less');
    var fileContent = fs.readFileSync(filePath, { encoding: 'utf8' });
    var parser = new BsLessdocParser(fileContent);
    return { sections: parser.parseFile() };
  };
  var generateRawFiles = require('./grunt/bs-raw-files-generator.js');
  var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
  var configBridge = grunt.file.readJSON('./grunt/configBridge.json', { encoding: 'utf8' });

  Object.keys(configBridge.paths).forEach(function (key) {
    configBridge.paths[key].forEach(function (val, i, arr) {
      arr[i] = path.join('./docs/assets', val);
    });
  });

  // Project configuration.
  grunt.initConfig({

    // Metadata.
    pkg: grunt.file.readJSON('package.json'),
    banner: '/*!\n' +
            ' * Bootstrap v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
            ' * Copyright 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
            ' * Licensed under the <%= pkg.license %> license\n' +
            ' */\n',
    jqueryCheck: configBridge.config.jqueryCheck.join('\n'),
    jqueryVersionCheck: configBridge.config.jqueryVersionCheck.join('\n'),

    // Task configuration.
    clean: {
      dist: 'dist',
      docs: 'docs/dist'
    },

    jshint: {
      options: {
        jshintrc: 'js/.jshintrc'
      },
      grunt: {
        options: {
          jshintrc: 'grunt/.jshintrc'
        },
        src: ['Gruntfile.js', 'package.js', 'grunt/*.js']
      },
      core: {
        src: 'js/*.js'
      },
      test: {
        options: {
          jshintrc: 'js/tests/unit/.jshintrc'
        },
        src: 'js/tests/unit/*.js'
      },
      assets: {
        src: ['docs/assets/js/src/*.js', 'docs/assets/js/*.js', '!docs/assets/js/*.min.js']
      }
    },

    jscs: {
      options: {
        config: 'js/.jscsrc'
      },
      grunt: {
        src: '<%= jshint.grunt.src %>'
      },
      core: {
        src: '<%= jshint.core.src %>'
      },
      test: {
        src: '<%= jshint.test.src %>'
      },
      assets: {
        options: {
          requireCamelCaseOrUpperCaseIdentifiers: null
        },
        src: '<%= jshint.assets.src %>'
      }
    },

    concat: {
      options: {
        banner: '<%= banner %>\n<%= jqueryCheck %>\n<%= jqueryVersionCheck %>',
        stripBanners: false
      },
      bootstrap: {
        src: [
          'js/transition.js',
          'js/alert.js',
          'js/button.js',
          'js/carousel.js',
          'js/collapse.js',
          'js/dropdown.js',
          'js/modal.js',
          'js/tooltip.js',
          'js/popover.js',
          'js/scrollspy.js',
          'js/tab.js',
          'js/affix.js'
        ],
        dest: 'dist/js/<%= pkg.name %>.js'
      }
    },

    uglify: {
      options: {
        compress: {
          warnings: false
        },
        mangle: true,
        preserveComments: 'some'
      },
      core: {
        src: '<%= concat.bootstrap.dest %>',
        dest: 'dist/js/<%= pkg.name %>.min.js'
      },
      customize: {
        src: configBridge.paths.customizerJs,
        dest: 'docs/assets/js/customize.min.js'
      },
      docsJs: {
        src: configBridge.paths.docsJs,
        dest: 'docs/assets/js/docs.min.js'
      }
    },

    qunit: {
      options: {
        inject: 'js/tests/unit/phantom.js'
      },
      files: 'js/tests/index.html'
    },

    less: {
      compileCore: {
        options: {
          strictMath: true,
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map'
        },
        src: 'less/bootstrap.less',
        dest: 'dist/css/<%= pkg.name %>.css'
      },
      compileTheme: {
        options: {
          strictMath: true,
          sourceMap: true,
          outputSourceFiles: true,
          sourceMapURL: '<%= pkg.name %>-theme.css.map',
          sourceMapFilename: 'dist/css/<%= pkg.name %>-theme.css.map'
        },
        src: 'less/theme.less',
        dest: 'dist/css/<%= pkg.name %>-theme.css'
      }
    },

    autoprefixer: {
      options: {
        browsers: configBridge.config.autoprefixerBrowsers
      },
      core: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>.css'
      },
      theme: {
        options: {
          map: true
        },
        src: 'dist/css/<%= pkg.name %>-theme.css'
      },
      docs: {
        src: ['docs/assets/css/src/docs.css']
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: ['**/*.css'],
        dest: 'docs/examples/'
      }
    },

    csslint: {
      options: {
        csslintrc: 'less/.csslintrc'
      },
      dist: [
        'dist/css/bootstrap.css',
        'dist/css/bootstrap-theme.css'
      ],
      examples: [
        'docs/examples/**/*.css'
      ],
      docs: {
        options: {
          ids: false,
          'overqualified-elements': false
        },
        src: 'docs/assets/css/src/docs.css'
      }
    },

    cssmin: {
      options: {
        // TODO: disable `zeroUnits` optimization once clean-css 3.2 is released
        //    and then simplify the fix for https://github.com/twbs/bootstrap/issues/14837 accordingly
        compatibility: 'ie8',
        keepSpecialComments: '*',
        sourceMap: true,
        advanced: false
      },
      minifyCore: {
        src: 'dist/css/<%= pkg.name %>.css',
        dest: 'dist/css/<%= pkg.name %>.min.css'
      },
      minifyTheme: {
        src: 'dist/css/<%= pkg.name %>-theme.css',
        dest: 'dist/css/<%= pkg.name %>-theme.min.css'
      },
      docs: {
        src: [
          'docs/assets/css/ie10-viewport-bug-workaround.css',
          'docs/assets/css/src/pygments-manni.css',
          'docs/assets/css/src/docs.css'
        ],
        dest: 'docs/assets/css/docs.min.css'
      }
    },

    csscomb: {
      options: {
        config: 'less/.csscomb.json'
      },
      dist: {
        expand: true,
        cwd: 'dist/css/',
        src: ['*.css', '!*.min.css'],
        dest: 'dist/css/'
      },
      examples: {
        expand: true,
        cwd: 'docs/examples/',
        src: '**/*.css',
        dest: 'docs/examples/'
      },
      docs: {
        src: 'docs/assets/css/src/docs.css',
        dest: 'docs/assets/css/src/docs.css'
      }
    },

    copy: {
      fonts: {
        expand: true,
        src: 'fonts/*',
        dest: 'dist/'
      },
      docs: {
        expand: true,
        cwd: 'dist/',
        src: [
          '**/*'
        ],
        dest: 'docs/dist/'
      }
    },

    connect: {
      server: {
        options: {
          port: 3000,
          base: '.'
        }
      }
    },

    jekyll: {
      options: {
        config: '_config.yml'
      },
      docs: {},
      github: {
        options: {
          raw: 'github: true'
        }
      }
    },

    htmlmin: {
      dist: {
        options: {
          collapseWhitespace: true,
          conservativeCollapse: true,
          minifyCSS: true,
          minifyJS: true,
          removeAttributeQuotes: true,
          removeComments: true
        },
        expand: true,
        cwd: '_gh_pages',
        dest: '_gh_pages',
        src: [
          '**/*.html',
          '!examples/**/*.html'
        ]
      }
    },

    jade: {
      options: {
        pretty: true,
        data: getLessVarsData
      },
      customizerVars: {
        src: 'docs/_jade/customizer-variables.jade',
        dest: 'docs/_includes/customizer-variables.html'
      },
      customizerNav: {
        src: 'docs/_jade/customizer-nav.jade',
        dest: 'docs/_includes/nav/customize.html'
      }
    },

    htmllint: {
      options: {
        ignore: [
          'Attribute "autocomplete" not allowed on element "button" at this point.',
          'Attribute "autocomplete" is only allowed when the input type is "color", "date", "datetime", "datetime-local", "email", "month", "number", "password", "range", "search", "tel", "text", "time", "url", or "week".',
          'Element "img" is missing required attribute "src".'
        ]
      },
      src: '_gh_pages/**/*.html'
    },

    watch: {
      src: {
        files: '<%= jshint.core.src %>',
        tasks: ['jshint:core', 'qunit', 'concat']
      },
      test: {
        files: '<%= jshint.test.src %>',
        tasks: ['jshint:test', 'qunit']
      },
      less: {
        files: 'less/**/*.less',
        tasks: 'less'
      }
    },

    sed: {
      versionNumber: {
        pattern: (function () {
          var old = grunt.option('oldver');
          return old ? RegExp.quote(old) : old;
        })(),
        replacement: grunt.option('newver'),
        exclude: [
          'dist/fonts',
          'docs/assets',
          'fonts',
          'js/tests/vendor',
          'node_modules',
          'test-infra'
        ],
        recursive: true
      }
    },

    'saucelabs-qunit': {
      all: {
        options: {
          build: process.env.TRAVIS_JOB_ID,
          throttled: 10,
          maxRetries: 3,
          maxPollRetries: 4,
          urls: ['http://127.0.0.1:3000/js/tests/index.html?hidepassed'],
          browsers: grunt.file.readYAML('grunt/sauce_browsers.yml')
        }
      }
    },

    exec: {
      npmUpdate: {
        command: 'npm update'
      }
    },

    compress: {
      main: {
        options: {
          archive: 'bootstrap-<%= pkg.version %>-dist.zip',
          mode: 'zip',
          level: 9,
          pretty: true
        },
        files: [
          {
            expand: true,
            cwd: 'dist/',
            src: ['**'],
            dest: 'bootstrap-<%= pkg.version %>-dist'
          }
        ]
      }
    }

  });


  // These plugins provide necessary tasks.
  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
  require('time-grunt')(grunt);

  // Docs HTML validation task
  grunt.registerTask('validate-html', ['jekyll:docs', 'htmllint']);

  var runSubset = function (subset) {
    return !process.env.TWBS_TEST || process.env.TWBS_TEST === subset;
  };
  var isUndefOrNonZero = function (val) {
    return val === undefined || val !== '0';
  };

  // Test task.
  var testSubtasks = [];
  // Skip core tests if running a different subset of the test suite
  if (runSubset('core') &&
      // Skip core tests if this is a Savage build
      process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'csslint:dist', 'test-js', 'docs']);
  }
  // Skip HTML validation if running a different subset of the test suite
  if (runSubset('validate-html') &&
      // Skip HTML5 validator on Travis when [skip validator] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_VALIDATOR)) {
    testSubtasks.push('validate-html');
  }
  // Only run Sauce Labs tests if there's a Sauce access key
  if (typeof process.env.SAUCE_ACCESS_KEY !== 'undefined' &&
      // Skip Sauce if running a different subset of the test suite
      runSubset('sauce-js-unit') &&
      // Skip Sauce on Travis when [skip sauce] is in the commit message
      isUndefOrNonZero(process.env.TWBS_DO_SAUCE)) {
    testSubtasks.push('connect');
    testSubtasks.push('saucelabs-qunit');
  }
  grunt.registerTask('test', testSubtasks);
  grunt.registerTask('test-js', ['jshint:core', 'jshint:test', 'jshint:grunt', 'jscs:core', 'jscs:test', 'jscs:grunt', 'qunit']);

  // JS distribution task.
  grunt.registerTask('dist-js', ['concat', 'uglify:core', 'commonjs']);

  // CSS distribution task.
  grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']);
  grunt.registerTask('dist-css', ['less-compile', 'autoprefixer:core', 'autoprefixer:theme', 'csscomb:dist', 'cssmin:minifyCore', 'cssmin:minifyTheme']);

  // Full distribution task.
  grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);

  // Default task.
  grunt.registerTask('default', ['clean:dist', 'copy:fonts', 'test']);

  // Version numbering task.
  // grunt change-version-number --oldver=A.B.C --newver=X.Y.Z
  // This can be overzealous, so its changes should always be manually reviewed!
  grunt.registerTask('change-version-number', 'sed');

  grunt.registerTask('build-glyphicons-data', function () { generateGlyphiconsData.call(this, grunt); });

  // task for building customizer
  grunt.registerTask('build-customizer', ['build-customizer-html', 'build-raw-files']);
  grunt.registerTask('build-customizer-html', 'jade');
  grunt.registerTask('build-raw-files', 'Add scripts/less files to customizer.', function () {
    var banner = grunt.template.process('<%= banner %>');
    generateRawFiles(grunt, banner);
  });

  grunt.registerTask('commonjs', 'Generate CommonJS entrypoint module in dist dir.', function () {
    var srcFiles = grunt.config.get('concat.bootstrap.src');
    var destFilepath = 'dist/js/npm.js';
    generateCommonJSModule(grunt, srcFiles, destFilepath);
  });

  // Docs task.
  grunt.registerTask('docs-css', ['autoprefixer:docs', 'autoprefixer:examples', 'csscomb:docs', 'csscomb:examples', 'cssmin:docs']);
  grunt.registerTask('lint-docs-css', ['csslint:docs', 'csslint:examples']);
  grunt.registerTask('docs-js', ['uglify:docsJs', 'uglify:customize']);
  grunt.registerTask('lint-docs-js', ['jshint:assets', 'jscs:assets']);
  grunt.registerTask('docs', ['docs-css', 'lint-docs-css', 'docs-js', 'lint-docs-js', 'clean:docs', 'copy:docs', 'build-glyphicons-data', 'build-customizer']);

  grunt.registerTask('prep-release', ['dist', 'docs', 'jekyll:github', 'htmlmin', 'compress']);

  // Task for updating the cached npm packages used by the Travis build (which are controlled by test-infra/npm-shrinkwrap.json).
  // This task should be run and the updated file should be committed whenever Bootstrap's dependencies change.
  grunt.registerTask('update-shrinkwrap', ['exec:npmUpdate', '_update-shrinkwrap']);
  grunt.registerTask('_update-shrinkwrap', function () {
    var done = this.async();
    npmShrinkwrap({ dev: true, dirname: __dirname }, function (err) {
      if (err) {
        grunt.fail.warn(err);
      }
      var dest = 'test-infra/npm-shrinkwrap.json';
      fs.renameSync('npm-shrinkwrap.json', dest);
      grunt.log.writeln('File ' + dest.cyan + ' updated.');
      done();
    });
  });
};

Everybody loved Grunt

  • Everybody was using Grunt
  • Lots of Grunt plugins existed for
    • uglifying code
    • minifying code
    • revisioning files
    • ....
  • But Grunt doesn't really solve the problem of bundling your assets and distributing your SPA.
  • It's plugin eco-system also became its Achilles-heel

New kid on the block

Gulp

var gulp = require('gulp');
var browserify = require('browserify');
var babelify = require('babelify');
var source = require('vinyl-source-stream');

gulp.task('build', function () {
    return browserify({entries: './app.jsx', extensions: ['.jsx'], debug: true})
        .transform('babelify', {presets: ['es2015', 'react']})
        .bundle()
        .pipe(source('bundle.js'))
        .pipe(gulp.dest('dist'));
});

gulp.task('watch', ['build'], function () {
    gulp.watch('*.jsx', ['build']);
});

gulp.task('default', ['watch']);

Gulp

  • Grunt was all about configuration over code
  • Gulp was all about code over configuration
  • Gulp was using Streams for efficient IO, and Unix Pipes (sources, filters, and sinks) to chain everything together.
  • Had less plugins but of higher quality and more focussed.
  • But still lots of boilerplate code was needed.

And some others came

But in the end : overload

Back to Webpack

Different ways of viewing an application

SCM view

  • Contains every single branch and tag since you started development
  • not only sources but also docs / notes / ...
  • Optimized for development and collaboration
  • Checkout this structure into your IDE and start developing
  • Ideal for source code distribution / not so much for binary distribution

IDE view

  • Here you might see ES6 code that can’t run in todays browsers, or typescript that needs to be transpired into javascript.
  • It will also contain all your dependencies (node modules)
  • It will contain tooling that helps you during development (hot-loading, webpack-dev-server,…)

Binary view

  • Bits & pieces  needed to run your app in a production environment
  • Only the minified, uglified, browser compatible code. (none of that ES6 stuff anymore)
  • Everything nicely bundled in
    a tar-bal, optimised for distribution and deployment

Distribution

  • Here you see npm used as the primary tool to construct / distribute / search and install packages.
  • We don’t distribute code, but we distribute binary assets / compiled code / transpired code / minified code …..

What is Webpack

  • A module bundler
  • Can work with lots of module systems (CommonJS / AMD / ES6)
  • Modules can be code (ex: ES6 classes), but also CSS resources , images , ...)
  • Webpack can see dependencies between modules
  • Will generate one or more bundles

Why do I need this ?

  • Use JavaScript & Common JS (Node JS components)
  • Transform ES6 / JSX / TypeScript 
  • Add Sourcemapping for uglified code
  • Bundle stylesheets (css, sass, less etc.)
  • Handle images and fonts
  • Make your code production ready

What does it bundle ?

  • Source code
    • ES6 class / import
    • CoffeeScript
    • TypeScript
  • CSS files / snippes
  • Image resouces
  • Fonts
  • HTML templates

 

index.html page

index.html

bundle.js

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>ReactJS and ES6</title>
</head>
<body>
<div id="content"></div>
<script src="dist/bundle.js"></script>
</body>
</html>

The idea is to have Webpack bundle everything in one or more bundles, and bootstrap them from an index.html

index.html page

index.html

bundle.js

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>ReactJS and ES6</title>
</head>
<body>
<div id="content"></div>
<script src="dist/vendor.js"></script>
<script src="dist/bundle.js"></script>
</body>
</html>

You are not limited to a single bundle

vendor.js

index.html page

index.html

bundle.js

<div>
   <a href="#">Home</a>
   <a href="#heavyPage">Heavy Page</a>
</div>

You are not limited to a single bundle

vendor.js

if (location.hash === '#heavyPage') {
    require.ensure([], function () {
      var HeavyPage = require('./heavyPage.js');
      React.render(HeavyPage(), 
          document.getElementById('app'));
});

heavy.js

vendor.js

/

#heavyPage

Source code and dependencies

import React from 'react';

class HelloWorld extends React.Component {
    render() {
        return <h1>Hello from {this.props.phrase}!</h1>;
    }
}

export default HelloWorld;
import React from 'react'
import { render } from 'react-dom'
import HelloWorld from './components/hello';

render(
    <HelloWorld phrase="ES6"/>,
    document.getElementById("content")
);

hello.js

index.js

var _componentsHello = __webpack_require__(160);
var _componentsHello2 = _interopRequireDefault(_componentsHello);
(0, _reactDom.render)(_react2['default'].createElement(_componentsHello2['default'], { phrase: 'ES6' }), document.getElementById("content"));
 *       return <div>Hello World</div>;
   *     return <div>Hello, {name}!</div>;
var HelloWorld = (function (_React$Component) {
	_inherits(HelloWorld, _React$Component);
	function HelloWorld() {
		_classCallCheck(this, HelloWorld);
		_get(Object.getPrototypeOf(HelloWorld.prototype), 'constructor', this).apply(this, arguments);
	_createClass(HelloWorld, [{
				'Hello from ',
	return HelloWorld;
exports['default'] = HelloWorld;

So that clean, de-composed ES6 code-base gets converted and bundled by Webback into something your browser understands.

How does it bundle ?

var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: [
    './src/index'
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: [ 'babel' ],
        exclude: /node_modules/,
        include: __dirname
      }
    ]
  }
}

Image resources

  • Images are as much a part of your code-base like javascript is
  • It should also be imported as-such
<img src={require('./images/exclamation.jpg')}/>
  • Webpack will see it as a dependency, and bundle it up
-rw-r--r--  1 ddewaele  wheel     127 Feb  3 15:59 index.html
-rw-r--r--  1 ddewaele  wheel  781572 Feb  3 15:59 bundle.js
-rw-r--r--  1 ddewaele  wheel   25829 Feb  3 15:59 7d24ad3a8e552ad2b43cde283e3125af.jpg

How does it load images ?

  • Webpack has the concept of loaders
  • Based on the file-extension, different loaders can be configured in your webpack configuration file.
{ 
	test: /\.gif$/, 
	loader: "url-loader?mimetype=image/png" 
},{
	 test: /\.(jpg|png)$/,
	 loader: 'url-loader?limit=1000' ,
}
<img class="img-remark img-responsive center-block" 
     src="data:image/jpeg;base64,iVBORw0KGgoAAAANS" 
     data-reactid=".0.0.1">  
<img src="9e31d0e1e181d1960982465bda6b24e1.png" 
     data-reactid=".0.0.0">

Stylesheets 

  • As with images, stylesheets can also be imported
import './Board.css';
{
	test: /\.css?$/,
	loader: "style-loader!css-loader",
	include: __dirname
},{ 
	test: /\.woff(2)?(\?v=[0-9].[0-9].[0-9])?$/,
	loader: "url-loader?mimetype=application/font-woff",
	include: __dirname
},{ 
	test: /\.(ttf|eot|svg)(\?v=[0-9].[0-9].[0-9])?$/, 
	loader: "file-loader?name=[name].[ext]",
	include: __dirname	
}

Webpack loaders

  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: [ 'babel' ],
        exclude: /node_modules/,
        include: __dirname
      }, {
        test: /\.css?$/,
        loader: "style-loader!css-loader",
        include: __dirname
      }, {
        test: /\.json$/,
        loader: 'json-loader',
        include: __dirname
      }, {
        test: /\.txt$/,
        loader: 'raw-loader',
        include: __dirname
      }, { 
         test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/, 
         loader: 'url-loader?limit=100000' 
      }]
  }
"devDependencies": {
    "babel-loader": "^6.2.0",
    "css-loader": "^0.23.1",
    "file-loader": "^0.8.5",
    "less-loader": "^2.2.2",
    "raw-loader": "^0.5.1",
    "style-loader": "^0.12.4",
    "url-loader": "^0.5.7",
    "webpack": "^1.12.9",
  },

package.json

webpack.config.json

Webpack Plugins

Webpacks comes with a set of plugins to help you with

  • Uglifying your code
  • Generating a bootstrap html
  • Minifying your code
  • Hot Module Replacement (HMR)
  plugins: [
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin(),
    new webpack.optimize.UglifyJsPlugin(),
    new HtmlWebpackPlugin({template: 'index.html'})
  ],

Webpack conclusion

  • Key take-away is that Webpack puts everything you need in the bundle.
  • The difference between dependencies and dev-dependencies becomes a semantic discussion
    • what is required for the build and the tests
    • what is required at runtime
-rw-r--r--  1 ddewaele  staff    25829 Feb  7 19:06 7d24ad3a8e552ad2b43cde283e3125af.jpg
-rw-r--r--  1 ddewaele  staff      127 Feb 15 22:48 index.html
-rw-r--r--  1 ddewaele  staff  1102191 Feb 15 22:48 bundle.js
-rw-r--r--  1 ddewaele  staff   108738 Feb 15 22:48 89889688147bd7575d6327160d64e760.svg

Installing webapps

All that's left todo is npm install your module

npm install @ixor/webapp1

> history@2.0.0 postinstall /home/user/node_modules/@ixor/webapp1/node_modules/history
> node ./npm-scripts/postinstall.js

@ixor/webapp1@0.0.15 node_modules/@ixor/webapp1

Webpack demo

What about Java ?

Integrating with Java

  • Sometimes its nice to stick with what you know.
  • We already use Java a lot, and know it well.
  • We have a lot of tooling for it in place.

But how do we integrate this javascript world with Java ?

Integrating with Java

  • Decide for yourself if it is needed.
  • Sometimes 1 uniform way of building apps can be useful
  • Both Maven and Gradle come with plugins for Node
  • The basic ideas are the same :
    • do not depend on local installs of Node / NPM
    • fix versions of node / npm
    • allow you to run node / npm scripts
    • integrates well with tools like bower / webpack

 

<plugin>
	<groupId>com.github.eirslett</groupId>
	<artifactId>frontend-maven-plugin</artifactId>
	<version>0.0.24</version>
	<configuration>
		<workingDirectory>src/main/resources/static</workingDirectory>
	</configuration>
	<executions>
		<execution>
			<id>install node and npm</id>
			<goals>
				<goal>install-node-and-npm</goal>
			</goals>
			<configuration>
				<nodeVersion>v0.10.33</nodeVersion>
				<npmVersion>1.3.8</npmVersion>
			</configuration>
		</execution>
		<execution>
			<id>npm install</id>
			<goals>
				<goal>npm</goal>
			</goals>
			<configuration>
				<arguments>install</arguments>
			</configuration>
		</execution>
		<execution>
			<id>webpack build</id>
			<goals>
				<goal>webpack</goal>
			</goals>
		</execution>
	</executions>
</plugin>
Install Node / NPM

	[INFO] --- frontend-maven-plugin:0.0.24:install-node-and-npm (install node and npm) @ react-and-spring-data-rest-basic ---
	[INFO] Installing node version v0.10.33
	[INFO] Creating temporary directory /Users/ddewaele/Projects/Node/maven/draft-tut-react-and-spring-data-rest/basic/node_tmp
	[INFO] Downloading Node.js from http://nodejs.org/dist/v0.10.33/node-v0.10.33-darwin-x64.tar.gz to /Users/ddewaele/Projects/Node/maven/draft-tut-react-and-spring-data-rest/basic/node_tmp/node.tar.gz
	[INFO] No proxy was configured, downloading directly
	[INFO] Extracting Node.js files in node_tmp
	[INFO] Unpacking /Users/ddewaele/Projects/Node/maven/draft-tut-react-and-spring-data-rest/basic/node_tmp/node.tar.gz into /Users/ddewaele/Projects/Node/maven/draft-tut-react-and-spring-data-rest/basic/node_tmp
	[INFO] Moving node binary to /Users/ddewaele/Projects/Node/maven/draft-tut-react-and-spring-data-rest/basic/node/node
	[INFO] Deleting temporary directory /Users/ddewaele/Projects/Node/maven/draft-tut-react-and-spring-data-rest/basic/node_tmp
	[INFO] Installed node locally.
	[INFO] Installing npm version 1.3.8
	[INFO] Downloading NPM from http://registry.npmjs.org/npm/-/npm-1.3.8.tgz to /Users/ddewaele/Projects/Node/maven/draft-tut-react-and-spring-data-rest/basic/npm.tar.gz
	[INFO] No proxy was configured, downloading directly
	[INFO] Extracting NPM files in node/
	[INFO] Unpacking /Users/ddewaele/Projects/Node/maven/draft-tut-react-and-spring-data-rest/basic/npm.tar.gz into /Users/ddewaele/Projects/Node/maven/draft-tut-react-and-spring-data-rest/basic/node
	[INFO] Installed NPM locally.
	[INFO] 
	[INFO] --- frontend-maven-plugin:0.0.24:npm (npm install) @ react-and-spring-data-rest-basic ---
	[INFO] Running 'npm install --color=false' in /Users/ddewaele/Projects/Node/maven/draft-tu

Install Bower dependencies

	[INFO] --- frontend-maven-plugin:0.0.24:bower (bower install) @ react-and-spring-data-rest-basic ---
	[INFO] Running 'bower install' in /Users/ddewaele/Projects/Node/maven/draft-tut-react-and-spring-data-rest/basic/src/main/resources/static
	[INFO] bower rest#~1.3.1           not-cached git://github.com/cujojs/rest.git#~1.3.1
	[INFO] bower rest#~1.3.1              resolve git://github.com/cujojs/rest.git#~1.3.1
plugins {
    id "com.moowork.node" version "0.11"
}

apply plugin: 'base'
version '0.0.1'

buildDir = 'dist'

node {
  version = '5.2.0'
  npmVersion = '3.3.12'
  download = true
}

task bundle(type: NpmTask) {
  inputs.dir(new File('assets'))
  inputs.dir(new File('src'))
  outputs.dir(new File('dist'))
  args = ['run', 'bundle']
}

task test(type: NpmTask) {
    args = ['test']
}

check.dependsOn(test)
bundle.dependsOn(npm_install)
assemble.dependsOn(bundle)
 ./gradlew bundle

:frontend:nodeSetup UP-TO-DATE
:frontend:npm_install
npm WARN EPACKAGEJSON boot-react-frontend@0.0.1 No repository field.
:frontend:bundle

> boot-react-frontend@0.0.1 bundle /Users/ddewaele/Projects/Node/grails/boot-react/frontend
> cross-env NODE_ENV=production webpack -p --optimize-dedupe

Hash: 2f3cebfa162062c6af48
Version: webpack 1.12.14
Time: 13058ms
         Asset       Size  Chunks             Chunk Names
     bundle.js     376 kB       0  [emitted]  main
    styles.css    13.1 kB       0  [emitted]  main
 bundle.js.map    3.26 MB       0  [emitted]  main
styles.css.map   87 bytes       0  [emitted]  main
    index.html  184 bytes          [emitted]  
   [0] multi main 28 bytes {0} [built]
    + 591 hidden modules
Child html-webpack-plugin for "index.html":
        + 3 hidden modules
Child extract-text-webpack-plugin:
        + 6 hidden modules

BUILD SUCCESSFUL

Total time: 29.14 secs

Thank you....

Made with Slides.com