Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active March 4, 2024 20:55
Show Gist options
  • Select an option

  • Save WebReflection/8840ec29d296f2fa98d8be0102f08590 to your computer and use it in GitHub Desktop.

Select an option

Save WebReflection/8840ec29d296f2fa98d8be0102f08590 to your computer and use it in GitHub Desktop.
NodeJS Executable Standalone Module

In this tweet I've shown how the incoming version of NodeJS could be used to run a standalone file as ESM through the flag --input-type=module.

This gist is an attempt to describe the technique I've used.

TL;DR - The Solution

#!/usr/bin/env sh
J=S//;echo "\n\n$(sed "1,2d" "$0")"|node --input-type=module "$@";exit $?
/* your 100% ESM code from this line on */
console.log('ESM');

The First line

It's a regular hashbang, that could usually be written either as #!/bin/sh or, in a wider compatible way, as #!/usr/bin/env sh, to carry on the environment, and execute via sh, or even bash.

This first line requires that the file is executable, something easy to obtain via chmod, as in chmod +x filename.

The Second Line

This part was inspired by cgjs executable.

Technically speaking, the J=S//; part of the string could be omitted, but then any JavaScript editor would show errors due usage of incompatible shell syntax.

Using J=S//; as prefix instead, would simply assign, in the shell world, as well as in the bash one, the string S// to a variable J, but it won't trigger errors, neither it will ever execute in the NodeJS world (no global J leak whatsoever).

However, it will start an inline comment, for your IDE of choice, so that no errors would be shown while editing such file.

In shell world, as well as in the bash one, it will simply keep executing whatever is after the semicolon.

Keep in mind, shell scripts, as well as bash scripts, execute as a stream, instead of requiring upfront parsing and syntax validation, meaning that exiting a file at any time won't cause syntax errors, even if the content after exiting is invalid.

The echo command

The echo command simply prints, usually via the standard output, some content.

The reason echo is part of this technique, is that we'd like to maintain the line number, in case any error occurs during NodeJS execution.

I'm not sure sed could do that for me right away, but echo works just as well.

It's important to use doublequotes to concatenate two new lines, "\n\n", and the received standard input, throught the pipe, via sed.

In shell, as well as in bash, double quotes can contain the output produced by system calls, as it is in the case of echo "1$(echo 2)3", which will simply output 123.

The sed part

The micro utility sed is a quite universal way to replace, hence transform, some content.

sed "1,2d" "$0"

Above command strips out the first two lines from the file specified by $0. In shell, as well as in bash, it's always good to expand variables that could contain spaces via double quotes, which is why $0 is wrapped as "$0", so that any path would be accepted.

In shell, as well as in bash, the $0 always refers to the file that is currently executing, like __filename would be in CommonJS.

The pipe

In shell, as in bash, the pipe operator | streams output produced by the left hand side, as right hand side input.

In this example, the pipe passes the echo output, including two \n and the output produced by sed, as node stdin.

To Recap

The echo "\n\n$(sed "1,2d" "$0")" will pass along an output containing two new lines \n plus whatever sed outputted before.

       as node stdin
     ┏━━━━━━━━━━━━━━┳━━▶
echo "\n\n$(sed ...)" | node ...

The node execution

After long discussions, it became obvious that the previously proposed flag --entry-type=module would've caused more confusion than expected, because it wasn't the equivalent to a "type": "module" field within a package.json.

Accordingly, since node --entry-type=module filename wouldn't have run filename as if it was within a module scoped as ESM via its package.json, they decide to remove such flag, and use instead one that would only accept an input, which is specially handy for evaluation cases via -e, or stdin.

But fear not, this technique perfectly addresses that stdin case, making the file itself, the source of the ESM code that will be executed.

node --input-type=module "$@"

Above shell or bash command, switches node input parsing goal as module, which practically means CommonJS is not available, and everything would run as pure ESM, even if an input couldn't really ever be used as module, but that's another story (naming is hard, and this whole module story is not scope of this gist).

The "$@" in shell, as well as in bash, would properly expand any optional extra arguments passed to the initial, shell, program.

The exit

Inspired by this suggestion, and coincidentally with the end of this gist, the last part of the technique ;exit $? simply splits, in two different commands, the previous node execution, and the exiting of the initial shell program itself, so that shell won't parse any extra content and it will exit passing along the same exit code node produced.

This is thanks to the $? special shell, and bash, variable, which simply contains the last exit code.

$ echo $?
0

$ trigger error
-bash: trigger: command not found

$ echo $?
127
@chardskarth
Copy link

chardskarth commented Aug 16, 2022

@WebReflection , this is awesome, thousand thanks for this! 🙏 💯

However, I can't seem to pass a param. The "$@" doesn't work and is instead treating params as if I was requiring/running other scripts.

tmp sPkJwmUv


EDIT, Workaround:

I was able to pass a parameter by adding a string substitution instead. and seeing that "$@" doesn't seem to work I removed it:

#!/usr/bin/env sh
J=S//;title=$1
J=S//;cd $( npm root -g ) && echo "\n\n$(sed -e "1,3d" -e "s/\$notestitle/'$title'/" "$0")"|node --input-type=module;exit $?
/* your 100% ESM code from this line on */

then added an undefined variable in my esm script, $notestitle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment