Build Pipeline - ProjectEvergreen/create-evergreen-app GitHub Wiki

Currently Project Evergreen is using a combination of webpack, Babel, and PostCSS in the reference app projects in its GitHub organization.

Note: More thorough investigation needs to be done to determine the pros / cons of using a module bundler vs shipping straight Modules.

General approach by Project Evergreen presently is to use a module bunder (webpack) + long term caching + HTTP/2.

Evergreen Build

weback

webpack is a module bundler for web applications. Out of the box it provides a lot of support for optimizing and code splitting JavaScript as well as managing many other assets types (like CSS, HTML, images, fonts, etc). It's principal function is it to understand the relationship of all the imports in your app so as to then be able to generate an optimized static build from that.

It also supports a robust community and ecosystem that allows the webpack bundling process to be extended to support many other important build related tasks, like:

  • Templating file paths into an index.html file
  • Generating Favicons
  • Inlining Critical CSS
  • Seamless integration with transpilation tools (like Babel and PostCSS)

How it Works

Esssentially, webpack works by being configured with an "entry" point file that contains the JavaScript needed to start your application, the "main" method if you will.

Learn more about webpack concepts here

A sample webpack configuration might look like this:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const webpack = require('webpack');

module.exports = {
  context: path.resolve(__dirname, 'src'),

  entry: {
    index: './index.jsx'
  },

  output: {
    path: path.resolve(__dirname, './build'),
    filename: '[name].[chunkhash].bundle.js',
    sourceMapFilename: '[name].map',
    chunkFilename: '[id].[chunkhash].js'
  },

  module: {
    rules: [{
      test: /\.(js*)$/,
      enforce: 'pre',
      loader: 'eslint-loader'
    }, {
      test: /\.(js*)x$/,
      loaders: [
        'babel-loader'
      ]
    }, {
      test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
      loader: 'url-loader?limit=10000&mimetype=application/font-woff'
    }, {
      test: /\.(ttf|eot|svg|jpe?g|png|gif|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
      loader: 'file-loader'
    }]
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html',
      chunksSortMode: 'dependency'
    })
  ]
};
  1. The "entry" file serves as the start of the dependency graph webpack will build up, following every import (including your node modules)
  2. Each file (by extension type) will get processed according to the configuration defined in module.rules, by a "loader". Loaders tell webpack how to process a given file. (converting LESS -> CSS, TypeScript -> JavaScript, )
  3. After all files have been processed, webpack will generate a compilation (an in memory representation of the final static output), and then run the plugins defined in plugins
  4. In the above example, HtmlWebpackPlugin will take the file paths from the compilation generated by webpack and have those paths inserted into a given index.html file. No more having to manage paths in <script> and <style> anymore! 🎉

Considerations

As powerful as webpack is, it does provide a lot of seemingly "magical" functionality out of the box, in particular its ability to use import to turn just about anything into a module (css, images, text files, etc). While this is convenient at build time and for development, being able to use import on non JavaScript modules is not part of the specification. For this reason, we urge developers to understand what webpack does that is spec compliant, and what it does that isn't.

Browserslist

Both the "env" presets available for Babel and PostCSS are made possible courtesy of an awesome tool called Browserslist and this makes this tool a core part of an evergreen build pipeline. Essentially, Browserlist allows querying of CanIUse data to determine based on the browser query provided, what features are / aren't available. This in turn allows Babel and PostCSS to intelligenty transpile only what's needed for the features that are missing, thus ensuring an "evergreen" experience for users and developers. Nice. 😎

To target modern evergreen browsers, use a .browserslistrc file like this:

> 1%
not op_mini all
not ie 11
$ npx browserslist
and_chr 67
and_uc 11.8
chrome 67
edge 17
firefox 61
ios_saf 11.3-11.4
ios_saf 11.0-11.2
safari 11.1

Babel

Babel is a compiler for JavaScript that transforms modern JavaScript down to a specific "target" of JavaScript. For example, source code could be written using 2018+ syntax, but transformed such that older browsers that don't support that syntax can still run that JavaScript.

.babelrc

For consisteny, all babel configuration should managed at the root of the project in a .babelrc file. A sample configuration that uses @babel/preset-env would look like this:

// babel.config.js
{
  "presets": ["@babel/preset-env"]
}

preset-env

When used with babel-preset-env, we can configure Babel with our target browser demographic, and then only transpile the code needed to support those browsers. In this way, as browsers mature, so will the generated JavaScript code, which is ideal since native features like import and class will only continue to get more performant over time as browser vendors continue to iterate on their JavaScript engines.

PostCSS (WIP)

PostCSS, much like Babel is a compiler, but for CSS! Just as with Babel, we can use modern CSS features without a transpilation process from a higher level version of CSS (LESS, SASS). CSS has finally arrived in modern web applications! ✨

postcss.config.js

For consistency, keep PostCSS configuration at the root of the project in postcss.config.js file. A sample configuration might look like:

module.exports = {
  plugins: {
    'postcss-cssnext': {},
    'cssnano': {}
  }
};

postcss-preset-env

When used with postcss-preset-env, we can configure PostCSS with our target browser demographic, and then only transpile the CSS features needed to support those browsers. Just as with our JavaScript, as browsers mature, so will the generated CSS code.

Polyfills

Depending on the browsers needing to be targetted, a few different polyfills might be needed

⚠️ If using a module bundler like webpack, polyfills should NOT be bundled. Instead use something like CopyWebpackPlugin or load them from a CDN via <script> tags instead (like we'll do in our below examples).

Web Components

Certain browsers don't have all the Web Components APIs available yet. Use a progressive enhancement based polyfill for these browsers, like webcomponents-bundle.

<!-- Web Components poyfill, e.g. for Firefox -->
<script src="//cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.0.2/webcomponents-bundle.js"></script>

ES5 / ES2015+ Support

Older browsers will likely not have all JavaScript features supported (like Function.proptype.toString()) and such will need polyfills.

<!-- JavaScript polyfills, e.g. FOR IE11 -->
<script src="//cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.0.0/polyfill.js"></script>

ES5 Adapter

However, if transpiling down to ES5 and because Custom Elements v1 must be class based, transpiling these v0 Custom Elements will break in modern browsers. This means a shim must be put in place to wrap these Custom Elements so they will This

<!-- Add forwards compatibility for ES5 transpiled web components, e.g. for Chrome, Safari -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.0.2/custom-elements-es5-adapter.js"></script>

📝 The error is known issue, as the () is used for progressive feature detection.

ESLint

Code quality and a consistent styleguide are important for any application. It's not so much about which is better, tabs or spaces for example, but rather which one everyone can agree to for the sake of the project, and then locking that style in with ESLint. We encourage making it a team effort so buy in can be assured from all contributors.

.eslintrc

Keep your .eslintrc file in the root of the project. Rather than show a sample config, here are some recommended configs to get you started:

General Quickstart

To target modern browsers using webpack, Babel, PostCSS, and LitElement see the below steps. IE Instructions included at the end.

  1. Install Dependencies (Yarn / npm) Install the following tools via Yarn / npm to get a package.json like this:
    "devDependencies": {
      "@babel/core": "^7.0.0",
      "@babel/preset-env": "^7.0.0",
      "babel-loader": "^8.0.1",
      "babel-plugin-transform-builtin-classes": "^0.6.1",
      "css-loader": "^0.28.11",
      "css-to-string-loader": "^0.1.3",
      "cssnano": "^3.10.0",
      "file-loader": "^1.1.11",
      "html-webpack-plugin": "^3.2.0",
      "postcss-cssnext": "^3.1.0",
      "postcss-loader": "^2.1.5",
      "url-loader": "^1.0.1",
      "webpack": "^4.8.3",
      "webpack-cli": "^2.1.3",
      "webpack-dev-server": "^3.1.4",
      "webpack-merge": "^4.1.2"
  2. Setup webpack
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const path = require('path');
    
    module.exports = {
      context: path.resolve('./src'),
    
      mode: 'production',
    
      /* start writing some code at src/index.js! */
      entry: {
        index: './index'
      },
    
      output: {
        filename: '[name].[chunkhash].bundle.js'
      },
      
      module: {
        rules: [{
          test: /\.js$/,
          loader: 'babel-loader'
        }, {
          test: /\.css$/,
          use: ['css-to-string-loader', 'css-loader', 'postcss-loader']
        }, {
          test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
          loader: 'url-loader?limit=10000&mimetype=application/font-woff'
        }, {
          test: /\.(ttf|eot|svg|jpe?g|png|gif|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
          loader: 'file-loader'
        }]
      },
    
      plugins: [
    
        new HtmlWebpackPlugin({
          chunksSortMode: 'dependency'
        })
      
      ]
    
    };
  3. Setup Browserslist (.browserslistrc)
    > 1%
    not op_mini all
    not ie 11
  4. Setup Babel (babel.config.js)
    module.exports = {
    
      presets: ['@babel/preset-env'],
      
      /* 
       * needed for LitElement 
       * https://github.com/WebReflection/babel-plugin-transform-builtin-classes 
       */
      plugins: [
        ['babel-plugin-transform-builtin-classes', {
          globals: ['LitElement']
        }]
      ]
      
    };
  5. Setup PostCSS (postcss.config.js)
    module.exports = {
      plugins: {
        'postcss-cssnext': {},
        'cssnano': {}
      }
    };
  6. Setup Polyfills
    <script src="//cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.0.2/webcomponents-bundle.js"></script>

For IE11

  1. In .browserslistrc, allow IE 11
    > 1%
    not op_mini all
    
  2. Include ES5 / ES2015+ polyfills for IE 11 and forward op of ES5 / v0 Custom Element
    <!-- Add forwards compatibility for ES5 transpiled web components -->
    <!-- https://github.com/Polymer/lit-element/issues/72#issuecomment-390894353 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.0.2/custom-elements-es5-adapter.js"></script>
    
    <!-- JavaScript polyfill FOR IE11 -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.0.0/polyfill.js"></script>
    
    <!-- Web Components poyfill for Firefox -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.0.2/webcomponents-bundle.js"></script>

Recommended Documentation

⚠️ **GitHub.com Fallback** ⚠️