Building a Preact application into a standalone script with Rollup

Rollup is a module bundler for JavaScript, made for speed and simplicity of setup. It also does tree-shaking (imports only the functionality you actually imported), supports JSX compilation and provides other neat functionality.

Step 1. (boring) Install NPM dependencies.

Let’s start with something that any JS project starts with – install every tiny package we will need during our development. This includes Rollup itself, the Livereload plugin for Rollup which will automatically reload the app on every file change, the Serve plugin for Rollup which will create a small static files server for the public/ folder, and a whole bunch of other plugins for React and Babel compiler:

npm i --save-dev @babel/core @babel/preset-react @rollup/plugin-alias @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-replace preact rollup rollup-plugin-livereload rollup-plugin-serve serve

Now let’s just wait..

All the plugins should be installed an ready. We can now proceed to our basic configuration.

Step 2. Configuration of run scripts and Rollup.

Let’s add some scripts to our package.json. We’ll add some basic ones like compiling React, compiling and watching React, serving the public directory, and doing all at once:

{
  ...
   "scripts": {
    "build": "rollup -c",
    "watch": "rollup -c -w",
    "dev": "npm run-script watch",
    "start": "serve public"
  }
}

This should totally cover our rather simple development process. Of course you can reorganize the scripts in your own way, but those are all the commands you will need to start.

Now on to the rollup configuration. We will create a rollup.config.js file where we need to export a config object with the basic settings, as well as with the settings for each Rollup plugin. Let’s just get to it and highlight the main aspects of the configuration:

import serve from "rollup-plugin-serve";
import livereload from "rollup-plugin-livereload";
import babel from '@rollup/plugin-babel';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import replace from '@rollup/plugin-replace';
import alias from '@rollup/plugin-alias';

module.exports = {
  input: 'src/index.jsx',
  output: {
    dir: 'public/dist/',
    format: 'iife', // 'cjs' if building Node app, 'umd' for both
    sourcemap: true,
  },
  plugins: [
    nodeResolve({
        extensions: [".js"],
    }),
    replace({
        'process.env.NODE_ENV': JSON.stringify( 'development' ),
        preventAssignment: true
    }),
    babel({
        presets: ["@babel/preset-react"],
        plugins: ["@babel/plugin-transform-react-jsx"],
        babelHelpers: 'bundled'
    }),
    commonjs(),
    serve({
        open: true,
        verbose: true,
        contentBase: ["", "public"],
        host: "localhost",
        port: 3000,
    }),
    livereload({ watch: "public" }),
    
    alias({
      entries: [
        { find: 'react', replacement: 'preact/compat' },
        { find: 'react-dom/test-utils', replacement: 'preact/test-utils' },
        { find: 'react-dom', replacement: 'preact/compat' },
        { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' }
      ]
    })
  ]
};

We do quite a lot here. The minimal setup involves input and output settings, where we provide the entry point of our app, and provide a path to the destination folder. We also provide a format option and set it to iife (Immediately Invoked Function Expression). This means the script will just run as soon as it’s loaded, which is how scripts run in a browser.

We are also loading a Babel plugin, which deals with JSX, setting up the Serve plugin to set up a static file server, Livereload to automatically reload the script after any change, add a plugin to allow CommonJS modules in a browser and so on. However, there is one very important plugin for our example, which is the alias. Alias replaces all mentions of react and react-dom with their Preact equivalents. This allows you to also import React modules and they should just work (not always of course, sometimes you will run into a wall, but that’s what development is about).

Step 3. App structure.

Let’s see how our index.jsx looks like:

import { h, render } from 'preact';
import App from './components/App.jsx';

// Inject our app into the DOM
render(App, document.getElementById('root'));

h is a Preact component constructor, you can read more about on the Preact getting started page. Our main App component will live in the components folder, and we render it into the #root element on any page where the script is loaded. Now the last step, our component in App.jsx should include React. That way the Babel compiler will properly load the React object, but it will load it from a preact package (remember alias?):

import React from 'react';

const App = <h1>Hello from Preact!</h1>;

export default App;

That’s it. Now create an HTML file in the public/ folder and load our script from dist/index.js:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Preact - Rollup Test</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <script src="./dist/index.js"></script>
  </body>
</html>

If you wish to play with some JavaScript and browse the code, visit the public repository described in this post at https://github.com/javascriptlove/preact-rollup.


Did you know that I made an in-browser image converter, where you can convert your images without uploading them anywhere? Try it out and let me know what you think. It's free.

Leave a Comment

Your email address will not be published. Required fields are marked *