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 Modules
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
export
ed, and here even as adefault
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
andargv.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 withimport.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"