Let's start with app.js, changing its name to app.mjs:
$ mv app.js app.mjs
Change the block of require statements at the top to the following:
import fs from 'fs-extra';
import url from 'url';
import express from 'express';
import hbs from 'hbs';
import path from 'path';
import util from 'util';
import favicon from 'serve-favicon';
import logger from 'morgan';
import cookieParser from 'cookie-parser';
import bodyParser from 'body-parser';
import DBG from 'debug';
const debug = DBG('notes:debug');
const error = DBG('notes:error');
import { router as index } from './routes/index';
// const users = require('./routes/users');
import { router as notes } from './routes/notes';
// Workaround for lack of __dirname in ES6 modules
const __dirname = path.dirname(new URL(import.meta.url).pathname);
const app = express();
import rfs from 'rotating-file-stream';
Then, at the bottom of the script, make this change:
export default app;
Let's talk a little about the workaround mentioned here. There were several global variables automatically injected by Node.js into CommonJS modules. Those variables are not supported by ES6 modules. The critical variable for Notes is __dirname, which is used in app.mjs in several places. The code change shown here includes a workaround based on a brand new JavaScript feature that is available starting in Node.js 10.x, the import.meta.url variable.
The import.meta object is meant to inject useful information into an ES6 module. As the name implies, the import.meta.url variable contains the URL describing where the module was loaded from. For Node.js, at this time, ES6 modules can only be loaded from a file:// URL on the local filesystem. That means, if we extract the pathname of that URL, we can easily calculate the directory containing the module, as shown here.
Why this solution? Why not use a pathname beginning with ./? The answer is that a ./ filename is evaluated relative to the process's current working directory. That directory is usually different from the directory containing the Node.js module being executed. Therefore it is more than convenient that the Node.js team has added the import.meta.url feature.
The pattern followed in most cases is this change:
const moduleName = require('moduleName'); // in CommonJS modules
import moduleName from 'moduleName'; // in ES6 modules
Remember that Node.js uses the same module lookup algorithm in both ES6 and CommonJS modules. A Node.js require statement is synchronous, meaning that by the time require finishes, it has executed the module and is returning its module.exports. By contrast, an ES6 module is asynchronous, meaning the module may not have finished loading, and you can import just the required bits of the module.
Most of the module imports shown here are for regular Node.js modules installed in the node_modules directory, most of which are CommonJS modules. The rule for using import with a CommonJS module is that the module.exports object is treated as if it were the default export. The import statement shown earlier name the default export (or the module.exports object) as shown in the import statement. For a CommonJS module imported this way, you then use it as you would in a CommonJS context, moduleName.functionName().
The usage of the debug module is effectively the same, but is coded differently. In the CommonJS context, we're told to use that module as follows:
const debug = require('debug')('notes:debug');
const error = require('debug')('notes:error');
In other words, the module.exports of this module is a function, which we immediately invoke. There isn't a syntax for ES6 modules to use the debug module in that fashion. Therefore, we had to break it apart as shown, and explicitly call that function.
The final point of discussion is the import of the two router modules. It was first attempted to have those modules export the router as the default value, but Express threw an error in that case. Instead, we'll rewrite these modules to export router as a named export and then use that named export as shown here.