Develop Node app in ES6 without Nodemon and Babel
I recently started a new Node.js project, and as a habit, I began by installing two familiar packages: nodemon
and babel
. The purpose was that I needed a way to hot reload my app while writing it in ES6 Module.
A tool we have gotten to know since the beginning of time for hot reloading is nodemon
. Also, since the default configuration of Node.js only supports common.js, we need a way to transpile our code back to common.js. Surely, support for ES6 modules in Node is behind --experimental-modules
and requires .mjs
extension (which is intrusive in my opinion).
Most tutorials will suggest Babel for the job. However, I think it is way too much for our purpose (Babel is more suitable to be used for browsers). It also removes the benefits of using ES6 (tree shaking).
Rollup to the rescue
Introducing Rollup.
Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application.
(note: application)
Get started by installing rollup
as a dev dependency.
yarn add rollup -D// ornpm i rollup --save-dev
Next, create rollup.config.js
. (You may use ES6 in this file)
export default { input: "api/server.js", output: { file: "bundle.js", format: "cjs", },};
By this config, we are taking our api/server.js
(or wherever your main script is), and output a CommonJS version of it.
Even though it is CommonJS after all, the exported file has gone through treeshaking. Also, since everything is compiled into one file, our code may run a bit faster in Node by eliminating the need to require different modules.
Just for reference, here is my api/server.js
, written in ES6.
import next from "next";import { createServer } from "http";import apolloServer from "./apollo-server";import app from "./app";import { connect as connectMongoDB } from "./db/mongo";
// httpconst port = process.env.PORT || "3000";const httpServer = createServer();
// nextconst nextApp = next({ dev: process.env.NODE_ENV !== "production" });const nextHandle = nextApp.getRequestHandler();
// apolloapolloServer.applyMiddleware({ app, path: "/api" });apolloServer.installSubscriptionHandlers(httpServer);
async function start() { await connectMongoDB(); await nextApp.prepare(); app.all("*", nextHandle); httpServer.on("request", app.handler); httpServer.listen({ port }, () => { console.log(`๐ Apollo API ready at :${port}${apolloServer.graphqlPath}`); console.log( `๐ Apollo WS ready at :${port}${apolloServer.subscriptionsPath}` ); });}
start();
Hot reloading
To achieve the functionality of nodemon
, we add a rollup plugin called @rollup/plugin-run
.
๐ฃ A Rollup plugin which runs your bundles in Node once they're built.
Using this plugin gives much faster results compared to what you would do with nodemon.
(In my experience using it, this plugin is faster than nodemon
)
yarn add @rollup/plugin-run -D// ornpm i @rollup/plugin-run --save-dev
(We will import the above package in rollup.config.js
, which may be complained by eslint
, you can either eslint-disable
the warning or add the package as a regular dependency).
Back in rollup.config.js
:
import run from "@rollup/plugin-run";
export const roll = rollup;
const dev = process.env.NODE_ENV !== "production";
export default { input: "api/server.js", output: { file: "bundle.js", format: "cjs", }, plugins: [dev && run()],};
We import @rollup/plugin-run
and include it in plugins
. Notice that this will only run in development (by checking process.env.NODE_ENV
).
Add scripts to package.json
{ "scripts": { "start": "node bundle.js", "build": "NODE_ENV=production rollup -c", "dev": "rollup -c -w" }}
Our start
script simply runs the output bundle.js
.
Our build
script runs rollup
setting NODE_ENV
to production. (you may need cross-env
in Windows)
Our dev
call rollup
. The flag -c
means using our config file rollup.config.js
. The flag -w
rebuilds our bundle if the source files change on disk. In fact, @rollup/plugin-run
does not do hot-reloading but only runs the Node process every time rollup
recompile.
What about .env
We often use .env
in development. @rollup/plugin-run
allows us to execute an argument. In rollup.config.js
, edit our run()
function.
run({ execArgv: ["-r", "dotenv/config"],});
This allows us to do node -r
(--require) dotenv/config
. See dotenv preload usage.
Integrate Babel
Even though we do not use Babel to transpile import/export
to require/module.exports
, there are cases when we still need it. For example, I use it for @babel/plugin-proposal-optional-chaining
, which enables optional chaining (this proposal is ๐ฅ btw).
The plugin we need is rollup-plugin-babel
yarn add -D @babel/core rollup-plugin-babel// ornpm i --save-dev @babel/core rollup-plugin-babel
We can now add it to rollup.config.js
.
import run from "@rollup/plugin-run";import babel from "rollup-plugin-babel";
const dev = process.env.NODE_ENV !== "production";
export default { input: "api/server.js", output: { file: "bundle.js", format: "cjs", }, plugins: [ babel(), dev && run({ execArgv: ["-r", "dotenv/config"], }), ],};
The default configuration of rollup-plugin-babel
will read from .babelrc
. However, if you are like me, who has .babelrc
not for the node server but for framework such as React or Next.js, you can opt out. Doing so by edit babel()
:
babel({ babelrc: false, plugins: ["@babel/plugin-proposal-optional-chaining"],});