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:

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 of require('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.

results matching ""

    No results matching ""