'detect whether ES Module is run from command line in Node

When using CommonJS modules in Node, you can detect whether a script is being run from the command line using require.main === module.

What is an equivalent way to detect whether a script is being run from the command line when using ES Modules in Node (with the --experimental-modules flag)?



Solution 1:[1]

Use

if (import.meta.url === `file://${process.argv[1]}`) {
  // module was not imported but called directly
}

See the MDN docs on import.meta for details.

Update Sep 27, 2021

Perhaps more robust, but involving an extra import (via Rich Harris)

import url from 'url'

if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
  // module was not imported but called directly
}

Solution 2:[2]

There is none - yet (it's still experimental!). Although the prevailing opinion is that such a check is a bad practice anyway and you should just provide separate scripts for the library and the executable, there is an idea to provide a boolean import.meta.main property for this purpose.

Solution 3:[3]

The module global variable will be defined in CommonJS, but won’t exist at all in an ES module. Yes, there is an inconsistency there, that ES modules are the things that don’t have module variables.

You can check for an undefined variable by seeing if typeof v is the string (not value!) 'undefined'.

That turns into:

const inCommonJs = typeof module !== 'undefined';

console.log(`inCommonJs = ${inCommonJs}`);

If we put that exact code into both .cjs and .mjs files, we get the correct answers:

$ node foo.mjs
inCommonJs = false
$ cp foo.mjs foo.cjs
$ node foo.cjs
inCommonJs = true

Solution 4:[4]

The other answers get close, but will miss the mark for a pretty typical usecase - cli scripts exposed by the bin property in package.json files.

These scripts will be symlinked in the node_modules/.bin folder. These can be invoked through npx or as scripts defined in the scripts-object in package.json. process.argv[1] will in that case be the symlink and not the actual file referenced by import.meta.url

Furthermore, we need to convert the file path to an actual file://-url otherwise it will not work correctly on different platforms.

import { realpathSync } from "fs";
import { pathToFileURL } from "url";

function wasCalledAsScript() {
    // We use realpathSync to resolve symlinks, as cli scripts will often
    // be executed from symlinks in the `node_modules/.bin`-folder
    const realPath = realpathSync(process.argv[1]);

    // Convert the file-path to a file-url before comparing it
    const realPathAsUrl = pathToFileURL(realPath).href;
  
    return import.meta.url === realPathAsUrl;
}

if (wasCalledAsScript()) {
  // module was executed and imported by another file.
}

I would have posted this as a comment on the accepted answer, but apparently I'm not allowed to comment with a fresh account.

Solution 5:[5]

I like import.meta.url === `file://${process.argv[1]}` , but it does not work in Windows inside bash shell. This is the alternative that is only checking the basename:

const runningAsScript = import.meta.url.endsWith(path.basename(process.argv[1]));

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Bergi
Solution 3 andrewdotn
Solution 4 gustavnikolaj
Solution 5 Slawa