For a recent project, I wanted to have some JS code (in multiple files) available for testing from the command-line with Node.js, but also to be able to load the same code into a web page to be invoked directly from a browser.

I've encountered this same issue before for my in-browser 8080 assembler and simulator project, and used a combination of CommonJS requires with a bundler tool to make it work. But we're in 2023 now, and CommonJS is supposed to be phasing out. So my goal for the new project was to do this using ES modules (ESM) and without any separate tooling.

Let's see how it works.

Project structure

Here's the structure of the js-gauss-spline project serving as our demo:

$ tree
.
├── eqsolve.js
├── package.json
├── plot.html
├── README.md
├── spline.js
└── test
    └── test.js

The files eqsolve.js and spline.js implement the functionality we want to both test on the command-line and import in the browser. Their functionality is exposed via exported functions.

Testing in Node.js

The test code lives in test/test.js, and it starts like this:

import assert from 'node:assert/strict';
import { solve } from '../eqsolve.js';
import { buildSplineEquations } from '../spline.js';

The exported functions we want to test are imported with relative paths. The file also imports Node's built-in assert functionality; it uses asserts directly, without any unit-testing framework.

To run the tests, simply invoke node (which has to be a recent version that properly supports ES modules):

$ node --version
v20.5.0
$ node test/test.js
success

Running in the browser

So far so good. Now let's import these files into a web application running in the browser; in our project, the main entry point is plot.html. It has more custom JS code, along with whatever HTML elements are needed. Here's the JS part that imports functions from eqsolve.js and spline.js:

<script type="module">
    import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
    import { buildSplineEquations } from "./spline.js";
    import { solve } from "./eqsolve.js";

    // ... more web-app JS code here
</script>

According to MDN, browsers have supported ES modules for quite a while now, so if you have a reasonably recent browser it should support script type="module" and import statements.

Note that our code here imports an additional JS library using ESM - d3, directly from a URL.

When testing this web page locally, opening it as a file with the file:/// scheme won't work; you'll get CORS errors in the browser console, because importing local files from the page is not supported. We'll need to serve the directory locally using a file server.

Luckily this is very easy to do with my static-server project [1]:

$ static-server .
2023/10/21 07:07:44.168573 Serving directory "." on http://127.0.0.1:8080

With this server running, opening http://127.0.0.1:8080/plot.html should successfully load everything.

Direct support of JS imports in the browser is a big step forward for the ecosystem; now it's easy to properly structure non-trivial web applications without requiring a separate build step with external tooling.


[1]If you don't have go installed, the NPM http-server package will work just as well.