Repository: mapbox/batfish
Pull request #240 (2018-03-14 17:39:46)

Add devBrowserslist option and --browser CLI arg
Also uses browserslist and devBrowserslist to configure babel-preset-env, not just Autoprefixer.

The goals here are:

- Reduce build time during development. Hopefully Babel will run faster since babel-preset-env should be applying fewer transforms.
- Reduce the distance between the source maps and the source files during development. This might make it more palatable to use faster but less powerful Webpack `devtool`s, which could in turn significantly speed up development builds.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d845a25..236a05d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## HEAD
+
+- **Add:** Add `devBrowserslist` option. **This will change the browser support of your development (not production) build.**
+- **Add:** Add `-b, --browsers` option for `batfish start`.
+- **Add:** `browserslist` and `devBrowserslist` affect babel-preset-env, not just Autoprefixer.
+
 ## 1.2.0
 
 - **Add:** Include `babel-plugin-transform-object-rest-spread` by default.
diff --git a/bin/batfish.js b/bin/batfish.js
index 5ec987d..155a70c 100755
--- a/bin/batfish.js
+++ b/bin/batfish.js
@@ -47,6 +47,10 @@ ${chalk.bold(`${chalk.magenta('start')} options`)}
                    is a glob relative to the root of your site.
   --production     Build as though for production.
   --no-clear       Do not clear the destination directory.
+  -b, --browsers   A comma-separated browserslist string
+                   specifying the browsers you want to support
+                   during this dev build. Or "false" if you
+                   want to support all your production browsers.
 
 ${chalk.bold(`${chalk.magenta('build')} options`)}
   -d, --debug      Build for debugging, not for production.
@@ -74,6 +78,8 @@ ${chalk.bold('Examples')}
     ${chalk.cyan('batfish start -i about/**')}
   Start but only build the /about/history page.
     ${chalk.cyan('batfish start --include about/history')}
+  Start and build only for Chrome 60+.
+    ${chalk.cyan('batfish start --browsers "Chrome >= 60"')}
 `;
 
 const cli = meow({
@@ -105,6 +111,10 @@ const cli = meow({
     include: {
       type: 'string',
       alias: 'i'
+    },
+    browsers: {
+      type: 'string',
+      alias: 'b'
     }
   }
 });
@@ -156,6 +166,8 @@ if (configPath) {
 
 if (cli.flags.production) {
   config.production = cli.flags.production;
+} else if (command === 'build') {
+  config.production = true;
 }
 if (cli.flags.debug) {
   config.production = !cli.flags.debug;
@@ -172,6 +184,10 @@ if (command === 'start' && cli.flags.include) {
 if (cli.flags.clear === false) {
   config.clearOutputDirectory = false;
 }
+if (command === 'start' && cli.flags.browsers) {
+  config.devBrowserslist =
+    cli.flags.browsers === 'false' ? false : cli.flags.browsers;
+}
 
 const projectDirectory = path.dirname(configPath);
 
diff --git a/docs/cli.md b/docs/cli.md
index 2aa61c6..eeb86c7 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -29,6 +29,10 @@ start options
                    is a glob relative to the root of your site.
   --production     Build as though for production.
   --no-clear       Do not clear the destination directory.
+  -b, --browsers   A comma-separated browserslist string
+                   specifying the browsers you want to support
+                   during this dev build. Or "false" if you
+                   want to support all your production browsers.
 
 build options
   -d, --debug      Build for debugging, not for production.
@@ -56,4 +60,6 @@ Examples
     batfish start -i about/**
   Start but only build the /about/history page.
     batfish start --include about/history
+  Start and build only for Chrome 60+.
+    batfish start --browsers "Chrome >= 60"
 ```
diff --git a/docs/configuration.md b/docs/configuration.md
index e3cdb02..b96591e 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -27,6 +27,7 @@ You can specify an alternate location.
   - [applicationWrapperPath](#applicationwrapperpath)
   - [stylesheets](#stylesheets)
   - [browserslist](#browserslist)
+  - [devBrowserslist](#devbrowserslist)
   - [pagesDirectory](#pagesdirectory)
   - [outputDirectory](#outputdirectory)
   - [temporaryDirectory](#temporarydirectory)
@@ -122,7 +123,27 @@ Default: `['> 5%', 'last 2 versions']`.
 
 A [Browserslist](https://github.com/ai/browserslist) value to specify which browsers you need to support.
 
-This option is used to process your CSS through [Autoprefixer].
+This option is used to process your CSS through [Autoprefixer] and determine which Babel transforms to apply through [babel-preset-env].
+
+**This option determines the browser support of your production build (`batfish build`).
+During development (`batfish start`), this will be overridden by [`devBrowserslist`]** (unless `devBrowserslist` is set to `false`).
+
+### devBrowserslist
+
+Type: `Array<string> | false`.
+Default: `['Edge >= 14', 'Firefox >= 52', 'Chrome >= 58', 'Safari >= 10', 'iOS >= 10.2'`].
+
+A [Browserslist](https://github.com/ai/browserslist) value to specify which browsers you need to support *with the development build* (`batfish start`).
+If this value is `false`, [`browserslist`] will be used for both production and development builds.
+
+This option is used to process your CSS through [Autoprefixer] and determine which Babel transforms to apply through [babel-preset-env].
+
+**This option determines the browser support of your development build only (`batfish start`).
+For production builds (`batfish build`), [`browserslist`] will be used**.
+
+You can also override this value from the CLI with the `--browsers` flag.
+If, for example, you spend *most* of your time developing in Chrome 60+, you can run `batfish start --browsers "Chrome >= 60"`, which would dramatically reduce the amount of transformation Babel performs.
+When you want to check all the browsers you support in production, you can run `batfish start --browsers false`.
 
 ### pagesDirectory
 
@@ -339,9 +360,13 @@ You can pass options to [babel-preset-env] with the option [`babelPresetEnvOptio
 Type: `Object`. [Options for babel-preset-env].
 
 [babel-preset-env] is always used.
-By default, it is passed no options.
-That means the latest standard syntax will be available (not `stage-x`): `es2015`, `es2016`, etc..
+By default, it receives the following options:
+
+- `useBuiltIns` is `true`.
+- `target.browsers` is your [`browserslist`] or [`devBrowserslist`] value (read more about those options to understand when each is used).
+
 Use this option to further customize your build by passing any of the other many [options for babel-preset-env](https://babeljs.io/docs/plugins/preset-env/#options).
+It your object will be merged with the defaults — so they will only be overridden if you deliberately do so.
 
 ### babelExclude
 
@@ -601,3 +626,7 @@ If `true`, more information will be logged to the console.
 [scroll-restorer]: https://github.com/mapbox/scroll-restorer
 
 [`webpackstaticignore`]: #webpackstaticignore
+
+[`devbrowserslist`]: #devbrowserslist
+
+[`browserslist`]: #browserslist
diff --git a/flow-typed/defs.js b/flow-typed/defs.js
index a2b05dc..e8eaf2c 100644
--- a/flow-typed/defs.js
+++ b/flow-typed/defs.js
@@ -16,6 +16,7 @@ declare type BatfishConfiguration = {
   applicationWrapperPath: string,
   stylesheets: Array<string | Array<string>>,
   browserslist: string | Array<string>,
+  devBrowserslist: string | Array<string>,
   pagesDirectory: string,
   outputDirectory: string,
   temporaryDirectory: string,
@@ -40,7 +41,7 @@ declare type BatfishConfiguration = {
   },
   includePromisePolyfill: boolean,
   inlineJs?: Array<InlineJsEntry>,
-  production?: boolean,
+  production: boolean,
   developmentDevtool: string | false,
   productionDevtool: string | false,
   clearOutputDirectory: boolean,
diff --git a/src/node/create-babel-config.js b/src/node/create-babel-config.js
index 15f714c..eaff1ad 100644
--- a/src/node/create-babel-config.js
+++ b/src/node/create-babel-config.js
@@ -1,7 +1,9 @@
 // @flow
 'use strict';
 
+const _ = require('lodash');
 const constants = require('./constants');
+const getEnvBrowserslist = require('./get-env-browserslist');
 
 function createBabelConfig(
   batfishConfig: BatfishConfiguration,
@@ -14,10 +16,23 @@ function createBabelConfig(
 } {
   const target = options.target || constants.TARGET_BROWSER;
 
-  const presetEnvOptions =
-    target === constants.TARGET_NODE
-      ? { useBuiltIns: true, targets: { node: 'current' } }
-      : batfishConfig.babelPresetEnvOptions || {};
+  let presetEnvOptions;
+  if (target === constants.TARGET_NODE) {
+    presetEnvOptions = { useBuiltIns: true, targets: { node: 'current' } };
+  } else {
+    const envBrowserslist = getEnvBrowserslist(
+      batfishConfig.browserslist,
+      batfishConfig.devBrowserslist,
+      batfishConfig.production
+    );
+    presetEnvOptions = batfishConfig.babelPresetEnvOptions || {};
+    if (presetEnvOptions.useBuiltIns === undefined) {
+      presetEnvOptions.useBuiltIns = true;
+    }
+    if (_.get(presetEnvOptions, ['targets', 'browsers']) === undefined) {
+      _.set(presetEnvOptions, ['targets', 'browsers'], envBrowserslist);
+    }
+  }
 
   const presets = [
     [require.resolve('babel-preset-env'), presetEnvOptions],
diff --git a/src/node/get-env-browserslist.js b/src/node/get-env-browserslist.js
new file mode 100644
index 0000000..dd8c37a
--- /dev/null
+++ b/src/node/get-env-browserslist.js
@@ -0,0 +1,13 @@
+// @flow
+'use strict';
+
+module.exports = function getEnvBrowserslist(
+  prodBrowserslist: string | Array<string>,
+  devBrowserslist: string | Array<string> | false,
+  isProduction: boolean
+): string | Array<string> {
+  if (devBrowserslist === false || isProduction) {
+    return prodBrowserslist;
+  }
+  return devBrowserslist;
+};
diff --git a/src/node/validate-config.js b/src/node/validate-config.js
index 77363ac..15be207 100644
--- a/src/node/validate-config.js
+++ b/src/node/validate-config.js
@@ -13,6 +13,7 @@ const pathType = require('path-type');
 const autoprefixer = require('autoprefixer');
 const errorTypes = require('./error-types');
 const joinUrlParts = require('./join-url-parts');
+const getEnvBrowserslist = require('./get-env-browserslist');
 
 // !!!
 // Whenever you add a new configuration property,
@@ -43,6 +44,10 @@ const configSchema = {
     validator: x => _.isString(x) || isArrayOf(_.isString)(x),
     description: 'string or array of strings'
   },
+  devBrowserslist: {
+    validator: x => _.isString(x) || isArrayOf(_.isString)(x) || x === false,
+    description: 'string, array of strings, or false'
+  },
   pagesDirectory: {
     validator: isAbsolutePathToExistingDirectory,
     description: 'absolute path to an existing directory'
@@ -225,6 +230,14 @@ function validateConfig(
     babelInclude: [],
     siteBasePath: '',
     browserslist: ['> 5%', 'last 2 versions'],
+    devBrowserslist: [
+      // Recent browsers supporting a lot of ES2015.
+      'Edge >= 14',
+      'Firefox >= 52',
+      'Chrome >= 58',
+      'Safari >= 10',
+      'iOS >= 10.2'
+    ],
     fileLoaderExtensions: [
       'jpeg',
       'jpg',
@@ -247,9 +260,12 @@ function validateConfig(
 
   const config = Object.assign({}, defaults, rawConfig);
 
-  const defaultPostcssPlugins = [
-    autoprefixer({ browsers: config.browserslist })
-  ];
+  const envBrowserslist = getEnvBrowserslist(
+    config.browserslist,
+    config.devBrowserslist,
+    config.production
+  );
+  const defaultPostcssPlugins = [autoprefixer({ browsers: envBrowserslist })];
 
   // Invoke transform function properties
   if (typeof config.fileLoaderExtensions === 'function') {


Test patch:
diff --git a/test/__snapshots__/create-babel-config.test.js.snap b/test/__snapshots__/create-babel-config.test.js.snap
index ee9b59e..0463ea5 100644
--- a/test/__snapshots__/create-babel-config.test.js.snap
+++ b/test/__snapshots__/create-babel-config.test.js.snap
@@ -1,10 +1,60 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`createBabelConfig false devBrowserslist is not used in non-production 1`] = `
+Array [
+  Array [
+    "./../node_modules/babel-preset-env/lib/index.js",
+    Object {
+      "targets": Object {
+        "browsers": "mock-browserslist",
+      },
+      "useBuiltIns": true,
+    },
+  ],
+  "./../node_modules/babel-preset-react/lib/index.js",
+]
+`;
+
+exports[`createBabelConfig non-false devBrowserslist is not used in production 1`] = `
+Array [
+  Array [
+    "./../node_modules/babel-preset-env/lib/index.js",
+    Object {
+      "targets": Object {
+        "browsers": "mock-browserslist",
+      },
+      "useBuiltIns": true,
+    },
+  ],
+  "./../node_modules/babel-preset-react/lib/index.js",
+]
+`;
+
+exports[`createBabelConfig non-false devBrowserslist is used in non-production 1`] = `
+Array [
+  Array [
+    "./../node_modules/babel-preset-env/lib/index.js",
+    Object {
+      "targets": Object {
+        "browsers": "mock-dev-browserslist",
+      },
+      "useBuiltIns": true,
+    },
+  ],
+  "./../node_modules/babel-preset-react/lib/index.js",
+]
+`;
+
 exports[`createBabelConfig with Batfish config that affects Babel 1`] = `
 Array [
   Array [
     "./../node_modules/babel-preset-env/lib/index.js",
-    Object {},
+    Object {
+      "targets": Object {
+        "browsers": "mock-browserslist",
+      },
+      "useBuiltIns": true,
+    },
   ],
   "./../node_modules/babel-preset-react/lib/index.js",
   "./a/b",
@@ -32,8 +82,7 @@ Array [
   ],
   "./foo/bar",
   "./baz",
-  "./../node_modules/babel-plugin-transform-react-jsx-source/lib/index.js",
-  "./../node_modules/babel-plugin-transform-react-jsx-self/lib/index.js",
+  "./../babel-plugin-transform-react-remove-prop-types",
 ]
 `;
 
@@ -56,7 +105,12 @@ exports[`createBabelConfig with defaults 1`] = `
 Array [
   Array [
     "./../node_modules/babel-preset-env/lib/index.js",
-    Object {},
+    Object {
+      "targets": Object {
+        "browsers": "mock-browserslist",
+      },
+      "useBuiltIns": true,
+    },
   ],
   "./../node_modules/babel-preset-react/lib/index.js",
 ]
@@ -75,7 +129,6 @@ Array [
       "remarkPlugins": undefined,
     },
   ],
-  "./../node_modules/babel-plugin-transform-react-jsx-source/lib/index.js",
-  "./../node_modules/babel-plugin-transform-react-jsx-self/lib/index.js",
+  "./../babel-plugin-transform-react-remove-prop-types",
 ]
 `;
diff --git a/test/__snapshots__/validate-config.test.js.snap b/test/__snapshots__/validate-config.test.js.snap
index 99410cc..f7e2bc1 100644
--- a/test/__snapshots__/validate-config.test.js.snap
+++ b/test/__snapshots__/validate-config.test.js.snap
@@ -12,6 +12,13 @@ Object {
     "last 2 versions",
   ],
   "clearOutputDirectory": true,
+  "devBrowserslist": Array [
+    "Edge >= 14",
+    "Firefox >= 52",
+    "Chrome >= 58",
+    "Safari >= 10",
+    "iOS >= 10.2",
+  ],
   "developmentDevtool": "source-map",
   "fileLoaderExtensions": Array [
     "jpeg",
diff --git a/test/create-babel-config.test.js b/test/create-babel-config.test.js
index 7f7e92c..85b6aea 100644
--- a/test/create-babel-config.test.js
+++ b/test/create-babel-config.test.js
@@ -8,7 +8,9 @@ const abs = x => path.join(__dirname, x);
 const defaultBatfishConfig = () => ({
   babelPlugins: [],
   babelPresets: [],
-  jsxtremeMarkdownOptions: {}
+  jsxtremeMarkdownOptions: {},
+  browserslist: 'mock-browserslist',
+  production: true
 });
 
 const relativizeBabelSetting = p => {
@@ -46,4 +48,34 @@ describe('createBabelConfig', () => {
     expect(actual.presets.map(relativizeBabelSetting)).toMatchSnapshot();
     expect(actual.plugins.map(relativizeBabelSetting)).toMatchSnapshot();
   });
+
+  test('non-false devBrowserslist is used in non-production', () => {
+    const actual = createBabelConfig(
+      Object.assign(defaultBatfishConfig(), {
+        production: false,
+        devBrowserslist: 'mock-dev-browserslist'
+      })
+    );
+    expect(actual.presets.map(relativizeBabelSetting)).toMatchSnapshot();
+  });
+
+  test('false devBrowserslist is not used in non-production', () => {
+    const actual = createBabelConfig(
+      Object.assign(defaultBatfishConfig(), {
+        production: false,
+        devBrowserslist: false
+      })
+    );
+    expect(actual.presets.map(relativizeBabelSetting)).toMatchSnapshot();
+  });
+
+  test('non-false devBrowserslist is not used in production', () => {
+    const actual = createBabelConfig(
+      Object.assign(defaultBatfishConfig(), {
+        production: true,
+        devBrowserslist: 'mock-dev-browserslist'
+      })
+    );
+    expect(actual.presets.map(relativizeBabelSetting)).toMatchSnapshot();
+  });
 });
diff --git a/test/validate-config.test.js b/test/validate-config.test.js
index e42cfc3..a072772 100644
--- a/test/validate-config.test.js
+++ b/test/validate-config.test.js
@@ -6,6 +6,7 @@ const path = require('path');
 const fs = require('fs');
 const stripAnsi = require('strip-ansi');
 const pathType = require('path-type');
+const autoprefixer = require('autoprefixer');
 const validateConfig = require('../src/node/validate-config');
 const errorTypes = require('../src/node/error-types');
 const projectRootSerializer = require('./test-util/project-root-serializer');
@@ -31,6 +32,9 @@ jest.mock('path-type', () => {
   };
 });
 
+// Mock autoprefixer so we can check which arguments it is passed.
+jest.mock('autoprefixer', () => jest.fn(() => () => {}));
+
 describe('validateConfig', () => {
   const projectDirectory = '/my-project';
 
@@ -145,6 +149,34 @@ describe('validateConfig', () => {
     ).toHaveProperty('publicAssetsPath', 'site_assets');
   });
 
+  test('if devBrowserslist is false, even non-production build uses browserslist to configure autoprefixer', () => {
+    const config = validateConfig(
+      { devBrowserslist: false, production: false },
+      projectDirectory
+    );
+    expect(autoprefixer.mock.calls[0][0]).toEqual({
+      browsers: config.browserslist
+    });
+  });
+
+  test('if devBrowserslist is not false, it is still ignored in production build when configuring autoprefixer', () => {
+    const config = validateConfig(
+      { devBrowserslist: 'Chrome >= 60', production: true },
+      projectDirectory
+    );
+    expect(autoprefixer.mock.calls[0][0]).toEqual({
+      browsers: config.browserslist
+    });
+  });
+
+  test('if devBrowserslist is not false, it is used in non-production build to configure autoprefixer', () => {
+    validateConfig(
+      { devBrowserslist: 'Chrome >= 60', production: false },
+      projectDirectory
+    );
+    expect(autoprefixer.mock.calls[0][0]).toEqual({ browsers: 'Chrome >= 60' });
+  });
+
   test('processed siteBasePath does not end with a slash unless it is only a slash', () => {
     expect(
       validateConfig(