React development workflow & release mgmt with NPM & Webpack

Who am I ?



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.4Does 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.jpgHow 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.svgInstalling 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/webapp1Webpack 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....
React Dev Workflow and Release Mgmt
By Davy De Waele
React Dev Workflow and Release Mgmt
A guide to Single Page Application development workflow and release mgmt
- 2,346