Compiling and Modern JavaScript
ES Next
Having to context switch between thinking in ES6 or 7 and ES5 makes for unnecessary brain load! In Breko-hub even the test files support ES Next features! Every file other than ~/src/server.entry.js
supports ES Next language features.
Breko-hub doesn't stop at ES6, there's support for full stage-0 features and JSX.
Server Code
Server code uses run-time babel compilation along with plug-ins defined in the .babelrc
file. This is also needed for live-updates when the client app hot-reloads and the server render needs to stay in sync.
Webpack Configs
Webpack has a hidden feature: when the config file has a suffix of a loader name, that loader loads to the config file. I.E. Config files with the suffix .babel.js
will have babel language features. Breko-hub uses this hidden feature to run the webpack config files in ES Next.
The webpack.production
config needs to use this feature as it's called used via the command line. The other webpack config files run through the node server and don't need this feature.
Client side application
Webpack compilation on the client app uses the Webpack babel-loader. For production you must run the build command, in development the server will run Webpack in memory of the server for hot-reloading.
Tests
Unit and functional tests enable ES6 by setting the mocha --compiler to js:babel-register
in the ~/scripts/test/unit.sh
and ~/scripts/test/func.sh
.
Stage 0 Features used in Breko-hub:
- Class and Property Decorators
- Module exports
- Async Functions
- Trailing commas in function parameter lists and calls
- Class Property Declarations
Webpack
The module bundler used for the client-side application is Webpack. It supports pluggable functionality that breko-hub uses merrily!
There are 3 Webpack config files:
src/config/webpack.development.config.js
src/config/webpack.production.config.babel.js
src/config/webpack.unit-test.browser.config.js
These configuration files define how import
and require
calls work. All three extend the src/config/webpack.base.config.js
to share common configuration.
The development config sets up hot-reloading and debugging. To start the app using this config, set NODE_ENV=development
in your .env
file, and then run npm start
.
The unit-test config is for running unit tests in the browser. This is a powerful way to develop as it gives a fast feedback loop along with any browser's development tools. To run unit tests in the browser, run npm test
and open a localhost page on the env PORT.
The production config comes into action when you run npm run build
. This build gets picked up if NODE_ENV=production
and under the command npm start
. This configuration prioritizes performance for the output bundles. Also, it does not make use of any development features such as hot-reloading.
Both development and unit-test configurations create output files to memory. The production produces real bundle files to the file system into the src/static
directory.
Babel
Breko-hub uses a .babelrc
for server side run-time compilation. There's no difference between any NODE_ENV
setting, it's always the same configuration. In contrast, in the client-side compilation there are different settings for dev and prod using Webpack.
Any babel plugins used for language features in client compile must also be in the server compilation.
// .babelrc
{
"presets": ["es2015", "react", "stage-0"],
"plugins": [
"add-module-exports",
"babel-root-import",
"react-require",
["provide-modules", {
"debug": "debug"
}],
"transform-decorators-legacy",
["module-resolver", {
"root": ["./src"]
}]
]
}
Server and Client plugins:
- Add-module-exports - Avoid the need for
.default
on the end ofrequire('module')
calls. - Babel root imports - Resolve any import prefixed with a tilde
~
from the project root. - React require - Imports React in any file that uses JSX.
- Provide-modules - Add an import call to every file, configured by options. Breko-hub uses it to add
import debug from 'debug'
to every file. - Transform-decorators-legacy - Adds support to the legacy version of decorators.
- module-resolver - Mimick the behaviour of webpack resolve options.
Clientside plugins:
- Lodash and Ramda - Optimization to allow convenient destructuring of modules without the overhead.
The client babel configuration is in src/config/webpack.base.config.js
at the bottom of the file. It lives outside of the base config so that it can be re-used in other Webpack configurations. You will see that it contains the lodash
and ramda
plugins for optimizing the client bundle.
// src/config/webpack.base.config.js
const babelLoaderConfig = {
test: /\.jsx?$/,
include: [ /src\/app/, /src\/config/, /src\/server/ ],
loader: 'babel',
query: {
'presets': [ 'es2015', 'react', 'stage-0' ],
'plugins': [
'add-module-exports',
'lodash',
'ramda',
'react-require',
[ 'provide-modules', {
'debug': 'debug',
} ],
'babel-root-import',
'transform-decorators-legacy',
],
},
}
export babelLoaderConfig;
This babel loader query is a duplicate of the .babelrc
so that the server and client both treat the application in the same way. The loader will recognize any file with an extension like .es
, .es6
, .js
or .jsx
; this is a similar configuration to the default babel behavior.
Babel-runtime
Even though node has growing support for some ES Next features -- Breko-hub uses babel-runtime
to achieve full support. During development, this is also useful for live-updates as they get compiled at runtime! But, during production, there is a slight unnecessary overhead when starting the server.
You can find the babel-runtime initialization inside the server-entry.js
file.
// src/server-entry.js
require('babel-polyfill')
require('babel-register')
require('./server-start')
The production run-time compilation happens at server start up. Then, for every request coming into the server, the compiled code is already in memory. This makes the overhead minimal, by making use of node's require
cache.
Module Resolution
Inside the .babelrc
, Breko-hub makes use of the module-resolver
plugin. This plugin configuration adds ~/src
to the resolve paths in node. Meaning, no need for relative imports! You can resolve any file starting from the src
folder.
// src/server/middleware/handleNotFound.js
import App from '../../app/components/App/App'
import NotFoundRoute from '../../app/routes/NotFoundRoute'
// becomes
import App from 'app/components/App/App'
import NotFoundRoute from 'app/routes/NotFoundRoute'
For the client side bundle, webpack adds src
as the root resolve directory. These two settings achieve the same goal, one for client and one for server.
// src/config/webpack.base.config.js
resolve: {
root: [ SRC ],
// ...
}
Development middleware
During development, koa-webpack-dev-middleware
compiles the client assets into memory. This also hosts a development server for assets within the koa app. You can find this in ~/src/helpers/hotReload.js
. The koa-hot-middleware
provides, believe it or not, hot reloading!
// src/helpers/hotReload.js
const compiler = webpack(webpackDevelopmentConfig)
app.use(require('koa-webpack-dev-middleware')(compiler, {
quiet: true,
noInfo: true,
stats: {
colors: true,
reasons: true,
},
publicPath: webpackDevelopmentConfig.output.publicPath,
}))
app.use(require('koa-webpack-hot-middleware')(compiler))
Client side hot reloading consists of a client and server. The server supplies chunks of code from this development server using web socket push events. The client script receives these chunks on every code change. The client hot-reloading script is part of the development bundle.
// src/config/webpack.development.config.js
export default {
...webpackConfig,
entry: {
...webpackConfig.entry,
head: [
...webpackConfig.entry.head,
'webpack-hot-middleware/client',
],
},
// ...
}
Here you can see the development config for Webpack extends the base config. Adding the webpack-hot-middleware/client
script into the head
bundle. The head bundle gets injected into the HTML <head>
section.