Home

EcmaScript Modules

ECMAScript modules are the most up-to-date standardized way to put js code bits into modularized code. Two details that separate CommonJS from EMCAScript syntactically are exports, imports.

EcmaScript is Different than CommonJS

Node docs are a great reference for details comparing the two:

  • esmodules (ecmaScript) are async, whereas commonjs require statements are syynchronous
  • esmodules do not (hopefully will at some point?!) have the same caching mechanism, so modules can't be cleared & re-populated during runtime

Build A Node Process To Leverage ECMAScript Module Syntax

Create A Module

Here, a simple version of the addTwo.js file (as built in a previous post) converted to an ECMAScript module:

// addTwo.mjs
export default function addTwo(a,b){ return a + b}

A few details to notice:

  • the filename suffix is now *.mjs (which is even recommended by V8, the js engine). This file extension update is not required when the next step is done, but could be helpful to reduce developer cognitive load during development by expressing with the filename that the file is intended to be a module
  • the function is exported, and here even as a default

Setup A Repo To Work With ESM

One detail is different in this package.json from the package.json in another post: the type keyword.

{
  "name": "my-add-two-repo",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1",
  },
  "author": "",
  "license": "ISC",
  "keywords": [],
  "description": "",
}

The type keyword is about "enabling" the esm syntax.
The "type" keyword instructs node to use ECMAScript modules && recognize *.mjs as javascript modules.

Set An ECMAScript Module To Discover Its Runtime Method

In the commonJS approach of "figuring out" a program's runtime approach, the require.main and the module can be used by node to "figure out" a file's runtime method of either being run directly through something like node addTwo.js or require('./addTwo').
In ECMAScript modules, though, require.main is not an available functionality, and setting a file to discover it's runtime approach requires can require more work.
In order to "discover" if an ecmascript module is ran by node or by import, a few things can be leveraged...

process.argv accesses command-line arguments

Let's look at a 2-line code snippet in order to see what process.argv does:

// argv.js
console.log('argv here')
console.log(process.argv)

Running this from the cli with node argv.js returns...

[
  '/usr/local/bin/node',
  '(<pwd/path/...>)/args.js'
]

What this shows is...

  • process.argv returns a list (array) of strings, where each item in the list correlates to one of the commands in the run arg (node and argv.js)
  • the first result represents the node runtime, located at /usr/local/bin/node
  • the second result represents the file that is run, located at the current working direct (here summarized) ending with the js file

Running the same program with something like node argv.js 123 water will return

[
  '/usr/local/bin/node',
  '(<pwd/path/...>)/args.js'
  '123',
  'water'
]

Notice that each argument in the cli run command gets its own string in the process.argv array.

import.meta.url shows a modules file: url

import.meta.url is a handy detail within a module.

Let's create a 2-file project and use the import.meta.url to see what this does.
Make the directory, demo-add-two. Cteate an index.mjs and an addTwo.mjs.
setup the index.js:

import addTwo from './addTwo.mjs`;

console.log(`index meta url: ${import.meta.url}`)
console.log(addTwo(3,2));

Setup the addTwo file:

// addTwo.js
console.log(`addTwo meta url: ${import.meta.url}`)
function addTwo(a,b){ return a + b };
export default addTWo;

Run the project and see the output in the cli:

cd demo-add-two

node index.mjs

addTwo meta url: file:///<demo-add-two-path>/addTwo.mjs
index meta url: file:///<demo-add-two-path>/index.mjs

This file url can be used in conjunction with other native node module functionalities to get the "real" path of the files:

// addTwo.js
// 1 import
import { fileURLToPath } from 'url';

// 2. do work
const fileUrl = import.meta.url
const thisFilesPath = fileURLToPath(importMetaUrl)

// 3. see output
console.log({
  fileUrl,
  thisFilesPath
})
function addTwo(a,b){ return a + b };
export default addTWo;

Now, the output will include the file-path that can be more usable by node's module system, as the fileURLToPath in this case more-or-less "strips" the file:// prefix from the file's path:

{
  fileUrl: "file:///<demo-add-two-path>/addTwo.mjs"
  thisFilesPath: "<demo-add-two-path>/addTwo.mjs"
}

One more precautionary measure could be used to "normalize" the url with the realpath module:

// addTwo.js
// 1 import
import { fileURLToPath } from 'url';
import { realpath } from 'fs/promises';

// 2. do work
const fileUrl = import.meta.url
const thisFilesPath = fileURLToPath(importMetaUrl)
const cleanFilePath = await realpath(thisFilesPath);

// 3. see output
console.log({
  fileUrl,
  thisFilesPath,
  cleanFilePath
})
function addTwo(a,b){ return a + b };
export default addTWo;

This should output something like:

{
  fileUrl: "file:///<demo-add-two-path>/addTwo.mjs",
  thisFilesPath: "<demo-add-two-path>/addTwo.mjs"
  cleanFilePath: "<demo-add-two-path>/addTwo.mjs"
}

process.argv can be leveraged to detect a files run approach

Lets make a small progam and incorporate process.argv to "figure out" how a module is run.
Here, a directory structure for the project that will add two numbers together:

my-adder        # directory
  index.js      # file
  package.json  # file
  add.mjs       # file

Run npm init -y at the root to fill out some template package.json contents.
Populate add.mjs

Discover A Module's Path

An Experimental Approach With import.meta.resolve

import.meta.resolve is currently in experimental stability, but here's a look at it!
(for a less experiment appraoch see the createRequire section below).

Let's create a simple project and illustrate how import.meta.resolve can be used and what it returns:

mkdir metaResolvePractice
cd metaResolvePractice
touch index.js
touch addTwo.mjs

npm init -y
// index.js
const addTwoPath = './addTWo.mjs'
const resolvedAddTwo = import.meta.resolve(addTwoPath)
console.log({resolvedAddTwo})

This can be run with something like node start -- --experimental-import-meta-resolve

Use A More Stable Approach With createRequire

A different approach to discover a modules path, less experimental than the import.meta.resolve approach, is to leverage createResolve:

  • use createRequire in combination with import.meta.url to create a "require" function, much like the native commonJS require function
  • use the new "require" function and its chained method require.resolve to get the location of the module (covered in the node docs as well as an "intro to modules" post)

With a node_modules dir, and something like express installed...

import { createRequire } from 'module';
const myRequire = createRequire(import.meta.url)
const resolvedExpress = myRequire.resolve('express')
console.log("resolvedExpress: ",resolvedExpress)

...that will return something like...

"<path-to-project>/node_modules/express/index.js"
Tags: