Wyrd sisters

CommonJS to ESM

by Ty Myrddin

Updated on January 3, 2022

The react-base app was originally bootstrapped with Create React App using --template typescript.

The available npm start, npm test and npm run build scripts work fine.

But adding an index.js with a mix of commonJS and ES modules and doing an import of .json files, is a problem.

Apparently modules are in transition but not all are ESM yet. We will hold our experimentations with such index.js files trying to use node-fetch and ngrok for now. We may come back to this later again. For now, we just used the Nginx on the droplet.

Troubles

Having added an index.js to the react-base project (node v16.13.1) for trying out ngrok:


    import env from './src/env/.env.json';
    const handler = require("serve-handler");
    const http = require("http");
    const ngrok = require("ngrok");
    const fetch = require("node-fetch");

... running node index.js throws an error and trying to solve that we could be going around in circles.

ES modules

The implementation is now stable and can be used with the earlier commonJS module system. (Statement valid for version 13.14.0 and above).

This means that files ending with .mjs or .js extensions (with the nearest package.json file with a field type) are treated as ES modules.

First run


    $ node index.js
    (node:686975) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
    [snip]
    SyntaxError: Cannot use import statement outside a module

Adding "type: module" to the package.json file enables ES6 modules. In ES6, the source code should use import syntax. Previous ES use require syntax.

Sticking to ES5

We changed the import in the index.js to a require and it worked, but a lot of other files now give the same error.

With type:module

In the to the index.js nearest parent package.json file, we added a top-level "type" field with a value of "module":


    // package.json
    {
      [snip]
      "type": "module"
      [snip]
    }

Problem solved, BUT, this gives another error message:


    TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".json" for /react-base/src/env/.env.json

Apparently, JSON module import is experimental? It is supported only in commonJS mode, not in modules.

Supposedly adding the flag —experimental-json-modules with node fixes this error and make commonJS and module mode works with .json file import.


    $ node --experimental-json-modules index.js

Results in this error:


    file:///react-base/index.js:3
    const handler = require("serve-handler");
                    ^

    ReferenceError: require is not defined in ES module scope, you can use import instead
    This file is being treated as an ES module because it has a '.js' file extension and '/react-base/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.

Renaming index.js to index.cjs and


    $ node --experimental-json-modules index.cjs

results in:


    SyntaxError: Cannot use import statement outside a module

And we are back at the beginning.

Rewriting imports

The index.js tries to use both import and require module patterns, which isn’t possible. We either have to rewrite the "require" statements in index.js to "imports", like:


    import pkg from 'serve-handler';
    const { handler } = pkg;

But serve-handler, http, and ngrok are all commonJS modules, and doing this can lead to unexpected effects. We tried, just for the heck of it.

So we undo the "type": "module", in package.json and rewrite the import env from './src/env/.env.json'; to a commonJS require: const env = require('./src/env/.env.json');:


    $ node index.js
    /react-base/index.js:5
    const fetch = require("node-fetch");
                  ^

    Error [ERR_REQUIRE_ESM]: require() of ES Module /react-base/node_modules/node-fetch/src/index.js from /react-base/index.js not supported.
    Instead change the require of /react-base/node_modules/node-fetch/src/index.js in /react-base/index.js to a dynamic import() which is available in all CommonJS modules.

node-fetch

And the next problem appears: node-fetch was converted to be an ESM only package in version 3.0.0-beta.10. It is now an ESM-only module and can not be imported with require. the async import() function from CommonJS to load node-fetch asynchronously:


    const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));

Or downgrade to node-fetch version 2.6.6, which is built with CommonJS. <=

For now


WHERE'S MY COW?! Wyrd Sisters