mapbox / underreact

A light weight app build system
Other
16 stars 4 forks source link
frontend-tooling

DEPRECATED!

As of April 2022, we are deprecating Underreact and do not recommend usage on new projects. Development will only occur on an exemption basis for existing, internal Mapbox use-cases.

New projects should use an alternative such as Create React App or Vite.


@mapbox/underreact

Minimal, extensible React app build system that you won't need to eject.

It's a pretty thin wrapper around Babel, Webpack, and PostCSS, and will never accumulate an ecosystem of its own. And it aims to be just as useful for production applications with idiosyncratic demands as for simple prototypes.

Table of contents

Installation

Requirements:

Install Underreact as a devDependency of your project:

npm install --save-dev @mapbox/underreact

If you are building a React application, you also need to install React dependencies:

npm install react react-dom

Add _underreact* to your .gitignore, and maybe other ignore files (e.g. .eslintignore). That way you'll ignore files that Underreact generates. (If you set the outputDirectory option, you'll want to ignore your custom output directory.)

Getting started

The bare minimum to get started

// src/index.js
console.log('hello world!');
npx underreact start
# or
node node_modules/.bin/underreact start

Getting started with React

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
  render() {
    return <div>Hello world</div>;
  }
}

const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(<App />, container);
npx underreact start
# or
node node_modules/.bin/underreact start

Usage

You should not install the Underreact CLI globally. Instead, install Underreact as a devDependency of your project and use the underreact command via npx, npm "scripts", or node_modules/.bin/underreact. The easiest way is probably to set up npm scripts in package.json, so you can use npm run start, npm run build, etc., as needed.

The CLI provides the following commands:

Tip: In this readme we frequently use the command npx, if you find it unfamiliar please read this blog post by npm.

Underreact configuration file

To configure Underreact, create an underreact.config.js file at the root of your project.

Please note that no configuration is necessary to get started. On most production projects you'll want to set at least a few of the configuration object properties.

Your underreact.config.js can export a function or an object.

Exporting an object

You can also directly export the configuration object. This is a great way to start tweaking Underreact's configuration. For example, in the code below we simply modify the siteBasePath:

// underreact.config.js
module.exports = {
  siteBasePath: 'fancy'
};

Exporting a function

You can also export a function that returns your configuration object.

This function is called with the following named parameters:

// underreact.config.js
/**
 * @param {Object} opts
 * @param {Webpack} opts.webpack - Underreact's version of Webpack. Use this as needed to apply core Webpack plugins like `PrefetchPlugin`, `IgnorePlugin`, and `SourceMapDevToolPlugin`, so that your project is not dependent on its own Webpack version.
 * @param {'start'|'build'|'serve-static'} opts.command - The current Underreact command.
 * @param {'production'|'development'} opts.mode - The current mode of Underreact.
 * @returns {Promise<Object> | Object}
 */
module.exports = function underreactConfig({ webpack, command, mode }) {
  return {
    /* Underreact configuration object */
  };
};

This approach is quite powerful, because you can also return a Promise or use an async function to generate configurations with asynchronous dependencies from the filesystem or Internet. For example:

// underreact.config.js
const path = require('path');
const downloadAssets = require('./scripts/fetchAssets');

module.exports = async function underreactConfig({ webpack, command, mode }) {
  const publicAssetsPath = 'public';
  await downloadAssets(path.resolve(publicAssetsPath));

  return {
    publicAssetsPath,
    webpackPlugins: [command === 'build' ? new webpack.ProgressPlugin() : null]
  };
};

Defining your HTML

Underreact is intended for single-page apps, so you only need one HTML page. If you are building a React application, you can also use it to define a div element for react-dom to mount your React component tree on.

You have 2 choices:

  1. Preferred: Provide the htmlSource configuration option, which is an HTML string, a Promise or a Function returning HTML string or promise, that resolves to an HTML string.
  2. Provide no HTML-rendering function and let Underreact use the default, development-only HTML document. You should only do this for prototyping and early development: for production projects, you'll definitely want to define your own HTML, if only for the <title>.

If you provide a Promise for htmlSource, you can use any async I/O you need to put together the page. For example, you could read JS files and inject their code directly into <script> tags, or inject CSS into <style> tags. Or you could make an HTTP call to fetch dynamic data and inject it into the page with a <script> tag, so it's available to your React app.

If you provide a Function for htmlSource, Underreact would call it with the named parameter basePath. This gives you the flexibility to load assets with a root relative URL. The example below shows how to load a favicon from your public directory:

// underreact.config.js
module.exports = {
  /**
   * @param {Object} opts
   * @param {Webpack} opts.basePath - the normalized value of your site's base path
   * @returns {Promise<string> | string} 
   */
  htmlSource: ({ basePath }) => `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>Words that rhyme with fish</title>
      <meta name="description" content="A website about words that rhyme with fish, like plish">
      <link rel="shortcut icon" href="https://github.com/mapbox/underreact/blob/main/${basePath}/img/favicon.ico" type="image/x-icon" />
    </head>
    <body>
      <div id="app">
        <!-- React app will be rendered into this div -->
      </div>
    </body>
    </html>
  `
};

Note: Underreact would automatically inject the relevant script and link tags to your HTML template.

In the example below, we are defining our HTML in a separate file and requiring it in underreact.config.js:

// underreact.config.js
const html = require('./html');

module.exports = function underreactConfig({ webpack, command, mode }) {
  return {
    htmlSource: html(mode)
  };
};

// html.js
const fs = require('fs');
const { promisify } = require('util');
const minimizeJs = require('./minimize-js');

module.exports = async mode => {
  // read an external script, which we will inline
  let inlineJs = await promisify(fs.readFile)('./path/to/some-script.js');

  if (mode === 'production') {
    inlineJs = minimizeJs(inlineJs);
  }

  return `
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>Words that rhyme with fish</title>
        <meta name="description" content="A website about words that rhyme with fish, like plish">
        <script>${inlineJs}</script>
      </head>
      <body>
        <div id="app">
          <!-- React app will be rendered into this div -->
        </div>
      </body>
      </html>
    `;
};

Modes

Underreact provides two different modes of execution: development and production

Development mode

The development mode is the default mode of the start command. This mode is meant to be used in a local development environment, ideally your computer. Underreact does a bunch of optimizations to make compilation as fast as possible and enable developer tools like hot reloading and live reloading.

You can use this mode by simply running underreact start:

npx underreact start
# or being explicit
npx underreact start --mode=development

You can also use this mode with the build command and then serve it with serve-static, if you want to perform quick inspection of unminified files.

npx underreact build --mode=development
# serve it
npx underreact serve-static

Warning: Do not host code generated by development mode in a production environment.

Production mode

This mode is geared towards running the build output in a production environment. Underreact performs a bunch of optimizations to make your application run fast and reduce the bundle size.

You can use this mode by simply running underreact build:

npx underreact build
# or being explicit
npx underreact build --mode=production

You can also use this mode with the start command, in case you need to debug a problem that does not show up in development mode (e.g. one caused by minification):

npx underreact start --mode=production

Babel

Out of the box Underreact doesn't require you to setup a babel.config.js file. It uses @mapbox/babel-preset-mapbox internally to provide a top-notch default configuration.

Exposing babel.config.js

There are many cases — for example, when using Jest — when you want a babel.config.js to exist at the root your project. In this case it is best to create a babel.config.js at the root of your project and install @mapbox/babel-preset-mapbox as a devDependency:

npm install --save-dev @mapbox/babel-preset-mapbox
// babel.config.js
module.exports = {
  presets: ['@mapbox/babel-preset-mapbox']
};

While you are free to use any Babel presets & plugins, we strongly recommend that you use @mapbox/babel-preset-mapbox, as it provides a good combination of presets and plugins that are necessary for any Underreact application to work properly. For more advanced configuration visit the documentation for @mapbox/babel-preset-mapbox.

Note: Underreact doesn't support .babelrc; please use babel.config.js. (Read more about the difference here).

Browser support and polyfills

One of the founding principles of the Internet is its ability to support a multitude of devices. With the ever changing JavaScript ecosystem, new features of the language coming yearly and it has become difficult to use them while also supporting older browsers. Underreact wraps tools that solve these problems for you.

Transpiling JavaScript syntax and vendor-prefixing CSS

In Underreact you can use the Browserslist notation to specify the browser versions that you want to support. By default, Underreact uses a query that supports all major browsers including ie 11. You can change this behaviour by customizing the browserslist property:

// underreact.config.js
module.exports = {
  // The % refers to the global coverage of users from browserslist
  browserslist: ['>0.25%', 'not ie 11']
};

In the example above we are setting browserslist to target all the browsers with greater than 0.25% market share but not IE 11. This information will be passed to Autoprefixer to add vendor prefixes to CSS and to Babel to transpile your JavaScript to ES5.

Polyfilling newer JavaScript features

By default, Underreact polyfills the following JavaScript features:

The above polyfills (combined with Babel's transpilation) allow you to freely use for..of loops, async functions, and the spread operator.

If your application needs any other polyfill (e.g. fetch), you can install it and import it at the top of your jsEntry file:

// src/index.js
import 'whatwg-fetch';

Using @babel/polyfill

If you don't care about bundle size and want to polyfill all standard JS, you can install @babel/polyfill and import it in your jsEntry file.

Warning: polyfill must be set to false to use @babel/polyfill and you should only import @babel/polyfill once and only once in your application.

Deployment environments

Using environment variables

Underreact allows you to inject environment variables into your client-side code at build time. You can set them up by using the environmentVariables option in your configuration.

// underreact.config.js
module.exports = {
  environmentVariables: {
    SERVER_URL: 'https://ketchup.com'
  }
};

Note: DEPLOY_ENV & NODE_ENV are special environment variables in Underreact, so cannot be set in Underreact configuration.

DEPLOY_ENV and NODE_ENV

A recommend way to use DEPLOY_ENV is set it in your npm scripts:

// package.json
{
  "scripts": {
    "build": "underreact run build", // if not set, DEPLOY_ENV will be set to `production` automatically
    "build:staging": "DEPLOY_ENV=staging underreact run build",
    "build:sandbox": "DEPLOY_ENV=sandbox underreact run build"
  }
}

Why set DEPLOY_ENV instead of NODE_ENV?

If you are used to using NODE_ENV to target different deployment environments, you should instead use DEPLOY_ENV, instead.

Underreact discourages setting NODE_ENV manually, as a number of libraries depend on its value and a wrong value could result in an unoptimized build. You should instead use Underreact's modes, which will set the right NODE_ENV for your app.

Configuration object properties

browserslist

Type: Array<string> | Object. A valid Browserslist value. Default:['>0.2%', 'not dead', 'not ie < 11', 'not op_mini all'].

This value is used by Autoprefixer to set vendor prefixes in the CSS of your stylesheets, and is used to determine Babel compilation via babel-preset-env.

You can also target different settings for different Underreact modes by sending an object:

// underreact.config.js
module.exports = {
  browserslist: {
    production: ['> 1%', 'ie 10'],
    development: ['last 1 chrome version', 'last 1 firefox version']
  }
};

compileNodeModules

Type: boolean | Array<string>. Default: true.

Many npm packages are now written in ES2015+ syntax, which is not compatible with all the browsers you may be supporting. So by default Underreact compiles all node_modules to ES5.

You can set compileNodeModules: false to disable compilation of node_modules, or pass an array of package names to selectively compile. In the example below we are only compiling the specified npm packages:

// underreact.config.js
module.exports = {
  compileNodeModules: ['p-finally', 'p-queue']
};

devServerHistoryFallback

Type: boolean. Default: false.

Set to true if you want to use HTML5 History for client-side routing (as opposed to hash routing). This configures the development server to fall back to index.html when you request nested routes.

Tip: This should only be intentionally turned on, when you know you're going to configure your server to allow for HTML5-History-powered client-side routing.

environmentVariables

Type: { [string]: string | number | boolean }.

Environment variables that you'd like to make available in your client-side bundle on process.env. For example, if you set environmentVariables: { ORIGIN: 'foo.com' }, you can use process.env.ORIGIN in your JavaScript.

hot

Type: boolean. Default: true.

Enable hot module reloading of Underreact. Read "How do I enable hot module reloading?" for more details.

htmlSource

Type: string|Promise<string>|Function<string | Promise<string>>. Default:see the default HTML.

The HTML template for your app, or a Promise that resolves to it. Read "Defining your HTML" for more details.

jsEntry

Type: string. Absolute path. Default: ${project-root}/src/index.js.

The entry JS file for your app. In a typical React app, this is the file where you'll use react-dom to render your app on an element.

In the default value, project-root refers to the directory of your underreact.config.js file.

liveReload

Type: boolean. Default: true.

Set it to false to prevent automatic reloading of your app on code changes. Switching off liveReload also disables hot reloading.

outputDirectory

Type string. Absolute path, please. Default: ${project-root}/_site/.

The directory where webesite files should be written.

You'll want to ignore this directory with .gitignore, .eslintignore, etc.

In the default value, project-root refers to the directory of your underreact.config.js file.

polyfill

Type: boolean. Default: true.

Whether or not to use Underreact's default polyfills. Read more at "Polyfilling newer JavaScript features".

port

Type: number. Default: 8080.

Preferred port for development servers. If the specified port is unavailable, another port is used.

postcssPlugins

Type: Array<Function>. Default: [].

All of the CSS that you import is run through PostCSS, so you can apply any PostCSS plugins to it. Underreact always runs Autoprefixer for you.

publicAssetsPath

Type: string. Default: underreact-assets.

The directory where Underreact assets will be placed, relative to the website's root.

By default, for example, the main JS chunk will be written to underreact-assets/js/main.chunk.js.

Tip: It's important to know about this value so you can set up caching and other asset configuration on your server.

publicDirectory

Type string. Absolute path, please. Default: ${project-root}/public/.

Any files you put into this directory will be copied, without processing, into the outputDirectory. You can put images, favicons, data files, and anything else you want in here. To reference these assets in your Javascript code, you can use the BASE_PATH environment variable. Read "How do I include SVGs, images, and videos?".

In the default value, project-root refers to the directory of your underreact.config.js file.

siteBasePath

Type: string. Default: '/'.

Path to the base directory on the domain where the site will be deployed. The default value is the domain's root. To help create valid links, Underreact exposes this value to your source code with an environment variable BASE_PATH. The table below gives an example of how Underreact sets BASE_PATH environment variable for a given siteBasePath value:

siteBasePath process.env.BASE_PATH
(not set) ""
"/" ""
"ketchup" "/ketchup"
"/ketchup" "/ketchup"
"/ketchup/" "/ketchup"

This normalization behaviour comes in handy when writing statements like process.env.BASE_PATH + '/my-path'. Read "How do I include SVGs, images, and videos?".

Underreact also passes this as a named parameter to the htmlSource function. Read "Defining your HTML" for more details.

Tip: There's a good chance your app isn't at the root of your domain. So this option represents the path of your site within that domain. For example, if your app is at https://www.special.com/ketchup/*, you should set siteBasePath: '/ketchup'.

stats

Type: string. Absolute path. Default: ``.

The directory where Webpack would write stats. By default, no stats file will be generated.

vendorModules

Type: Array<string>. Default: [].

Identifiers of npm packages that you want to be added to the vendor bundle. The purpose of the vendor bundle is to deliberately group dependencies that change relatively infrequently — so the vendor bundle can stay cached for longer than the others.

By default, the vendor bundle includes react and react-dom.

Tip: It is good idea to include big stable libraries your project depends on: for example, redux, moment.js, lodash, etc.

webpackConfigTransform

Type: config => transformedConfig. Default x => x (identify function).

If you want to make changes to the Webpack configuration beyond what's available in the above options, you can use this, the nuclear option. Your function receives the Webpack configuration that Underreact generates and returns a new Webpack configuration, representing your heart's desires.

Tip: You should think twice before using webpackConfigTransform, as Underreact tries its best to abstract away Webpack so that you can focus on your application.

webpackLoaders

Type: Array<Rule>.

Webpack Rules specifying additional loaders that you'd like to add to your Webpack configuration.

If you need more fine-grained control over the Webpack configuration, use webpackConfigTransform.

Tip: You should be careful before adding support for a new source type (for example, scss, less, ts), as it will make your application dependent on Webpack and its ecosystem.

webpackPlugins

Type: Array<Object>.

Additional plugins to add to your Webpack configuration.

For plugins exposed on the webpack module itself (e.g. webpack.DefinePlugin), you should use Underreact's version of Webpack instead of installing your own. That will prevent any version incompatibilities. That version is available in the context object passed to your configuration module function.

Here, for example, is how you could use the DefinePlugin in your underreact.config.js:

// underreact.config.js
module.exports = ({ webpack }) => {
  return {
    webpackPlugins: [new webpack.DefinePlugin(..)]
  };
}

FAQs

How do I make Jest use Underreact's Babel configuration ?

Jest expects a babel.config.js at the root of your application. Read "Exposing babel.config.js". Underreact will only work with Jest version >=23.6. To install Jest, follow the steps mentioned for Babel 7 in the official installation docs.

How do I dynamically import JavaScript modules or React components?

You can use the import() syntax to asynchronously load a valid JavaScript module. For example:

// src/index.js
import("./math").then(math => {
  console.log(math.add(16, 26)); // 42
});
// src/math.js
export default add(a,b) {
  return a + b;
}

Read official React docs for more information on how to load your React component dynamically.

How do I reduce my build size?

To reduce the build size you can try the following:

How do I include SVGs, images, and videos?

import logo from './logo.png';
console.log(logo); // /logo.84287d09.png

function Header() {
  // Import result is the URL of your image
  return <img src={logo} alt="Logo" />;
}

It is generally a good idea to use the above method for importing assets because:

If you cannot use this method, you can place assets in the publicDirectory and create a link using the BASE_PATH environment variable as shown below:

function Header() {
  return <img src={process.env.BASE_PATH + '/logo.png'} alt="Logo" />;
}

The BASE_PATH environment variable is automatically set by Underreact and is equivalent to the value of siteBasePath. BASE_PATH's value will never end with a /, even if your siteBasePath does.

How do I enable hot module reloading ?

Hot module reloading allows you to reload only the module that has changed, without affecting the rest of the code or reloading the page in the browser. This is different from liveReload which reloads the entire application when code changes. Underreact first tries to hot reload, then falls back to live reloading.

Underreact supports CSS and JavaScript hot reloading. CSS hot reloading should work out of the box. To implement hot reloading for JavaScript modules, you can follow the steps in the Webpack docs. (You can skip the parts about Webpack configuration, as it has already been taken care of by Underreact.)

For React apps, you'll' benefit from hot module reloading of React components. Luckily this setup is fairly straightforward. First, you need to get your own babel.config.js file by following the steps in "Exposing babel.config.js". Then, you need to install react-hot-loader:

npm install react-hot-loader

And then add it to your babel.config.js:

// babel.config.js
module.exports = {
  presets: ['@mapbox/babel-preset-mapbox'],
  plugins: ['react-hot-loader/babel']
};

You can then make any of your React components hot:

// src/app.js
import React from 'react';
import { hot } from 'react-hot-loader';

const App = () => <div>Hello World!</div>;

export default hot(module)(App);

You can read more about hot reloading your React components by reading react-hot-loader docs.