Cross-platform shell instruments for Node.js

In July 2022, I launched dax
for Deno offering a cross-platform shell
for JavaScript written in JavaScript:
const knowledge = $.path("knowledge.json").readJsonSync();
await $`git add . && git commit -m "Launch ${knowledge.model}"`;
That is related and impressed by zx, however as a result of
it makes use of a cross-platform shell with frequent built-in cross-platform instructions,
extra code goes to work the identical manner on completely different working programs.
Initially, I wrote dax for Deno as a result of Deno is by far one of the best JavaScript
runtime for single file scripting鈥攁ll dependencies may be expressed within the
script file itself together with npm dependencies; there isn’t any node_modules
folder
(much less litter), and no separate set up command crucial.
As soon as written, dax used APIs that solely labored on Deno and making a Node.js
distribution was a good quantity of labor.
These days, Node.js has improved in its assist for Internet APIs and enhancements to
dnt (a device I created for constructing Deno
modules for Node) have made sustaining a Node.js distribution a lot simpler.
As a consequence of this, I am comfortable to say that dax is now obtainable on npm for customers of
Node.js:
import $ from "dax-sh";
await $`echo 'Hiya from dax!'`;
$ npm set up --save-dev dax-sh
$ node instance.mjs
Hiya from dax!
$ time node instance.mjs
Hiya from dax!
node instance.mjs 0.08s person 0.01 system 98% cpu 0.090 whole
You possibly can take a look at dax’s documentation right here for extra particulars:
https://github.com/dsherret/dax
A part of what kicked off my need to create a Node.js distribution for dax was
the discharge of Bun’s
shell, which
credits
dax as a supply of inspiration.
This led to requests for dax to be baked into Deno’s runtime.
For my part, this may be a step backwards for dax and never a great long run
determination for Deno.
I wish to clarify why I believe this and it might be fascinating to listen to your
suggestions. Notice these are my private opinions and never the opinions of the Deno
crew (which I am a member, however dax is a private undertaking I work on in my private
time).
Coupling a posh API like dax to the runtime means you may now not improve
them independently. With the ability to rely upon a particular model of dax and a
particular model of your runtime is a large profit. It means you may freely
improve your runtime model and the code utilizing dax will largely possible hold
working too鈥攖he prospect of encountering a brand new dax bug whereas upgrading your
runtime could be very low as a result of they’re decoupled.
Moreover, it additionally means while you improve your runtime, you need not
additionally improve all of your dax code on the identical time in case there is a breaking
change.
It additionally means you possible need not inform individuals to make use of a sure model of
Deno with a purpose to get the newest dax options (“hey, why would not this work? Oh,
that dax function is barely in Deno model x.x.x”). As an alternative, the code specifies
the dax model it is dependent upon so while you execute it, it possible works or dax can
present particular error messages for the runtime when not.
With the ability to use the identical API in several runtimes is a large profit. It
lowers vendor lock-in danger and lowers the complexity when working with a number of
runtimes as a result of the APIs you are utilizing are the identical. It additionally means when the following
nice runtime comes round you are not locked in with all this code relying on
a particular runtime (or a particular model of a particular runtime 馃槺).
When dax is revealed as a library, you may change runtimes and nonetheless rely upon
the identical model of dax.
Dax isn’t solely a shell, however a set shell instruments. It is a swiss military knife
that gives opinionated methods of doing frequent duties you want to do in
automation scripts. It has APIs for…
- progress and choice,
- making URL requests,
- logging,
- coping with paths,
- and sooner or later, CLI argument parsing and work caching.
All these APIs work along with one another and the shell. They’re opinionated
for simplicity. Baking opinionated APIs right into a runtime would not be a good suggestion
as a result of individuals have completely different opinions and opinions change over time. Within the
case of dax being a library, another person can come alongside and enhance on its API
or make one thing higher sooner or later, at which level dax can grow to be a relic
identical to previous JS frameworks.
One suggestion is to chop the scope of dax again to a shell solely moderately than a
assortment of shell instruments, however the shell continues to be fairly giant. For instance, you
can construct your personal customized $
to suite your wants and inject your personal customized
shell instructions written in JavaScript.
Chopping it again additional to not embrace that and another options is feasible,
however the shell itself continues to be fairly intricate and there is numerous tiny design
choices which can be higher left to a library like dax to get unsuitable after which be
improved upon by a future library or future main model of dax. Additionally at a
sure level scope will get in the reduction of sufficient that it begins turning into much less helpful.
I am nonetheless slowly determining an applicable API for dax. I do not imagine
something goes to alter drastically, however making a mistake if it had been a
built-in runtime API could be deadly. Constructed-in APIs and the selections made ought to
ideally be everlasting. After they’re not everlasting or get eliminated, that creates a
lot of complications.
When it is in a library, it is behind a individually versioned API, so the prospect of
your code not working with the runtime anymore is slim, and making breaking
modifications in library that is behind a versioned API is rather more manageable.
Think about if the same API to dax had been built-in into the runtime that made
the error of spawning the system shell as a result of we hadn’t thought to make it
cross platform but? Picture what different potentialities for this API we’ll uncover
sooner or later and be glad we will simply make the modifications to enhance it as a result of
it exists as a library.
A part of the argument to combine this API into the runtime is for efficiency,
however dax begins up in 90ms on my machine in Node.js and 70ms in Deno. It executes
instructions virtually as quick as utilizing Deno’s Command
API (2ms slower on my
machine). May or not it’s sooner? In all probability… I have never finished any in depth
benchmarking on dax as a result of I develop it in my free time round all the opposite
tasks I do.
It is quick sufficient for my wants. You’d positively have the ability to present it being slower
than some native code in a scorching loop, however typically automation scripts solely
execute a handful of instructions (perhaps ~10 instructions) and spend most of their time
ready for lengthy advanced duties to complete (for me, stuff like cargo construct
), so
gaining some milliseconds by it being built-in and native would not assist a lot in
most actual world scripts.
Plus being much less productive writing automation scripts with a much less featureful API
will burn up way more of your time than the few milliseconds saved with it being
built-in, which will not even be meaningfully saved in most actual world situations.
If we’re optimizing for efficiency solely, dax really would not have to be
built-in and will go native utilizing Deno’s FFI assist, however in my view
creating much less moveable much less auditable code written in a language not as many
individuals perceive to have a barely higher efficiency expertise is a foul
commerce.
I would not categorize having no dependency as a comfort as a result of the runtime
coupling I talked about in a earlier part results in inconvenience. Possibly it is
barely annoying in Node.js as a result of it requires including `dax-sh“ to a
package deal.json and putting in it, however in Deno you may simply write:
#!/usr/bin/env -S deno run -A
import $ from "https://deno.land/x/dax/0.39.0/mod.ts";
await $`echo Hiya`;
Is writing that tough? I do not imagine so, and now my script has all of the
data to know what model of dax to make use of or I can swap it out for a
related dependency that has the API I like as an alternative.
It is nice in Deno as a result of I do not even have to run a separate set up script鈥擨
simply run that script straight and it’ll use the model I specified. Of
course, I might use a naked specifier like "dax"
by making a deno.json with
an embedded import map to make
import $ from "dax";
work:
{
"imports": {
"dax": "https://deno.land/x/dax/0.39.0/mod.ts"
}
}
Total, I get the need for having dax built-in, however I do not imagine it is the
proper long run determination. Maybe if there is a need for a shell solely and never a
swiss military knife of automation scripts, then the core performance in dax might
be extracted out to a less complicated package deal on the upcoming
JSR registry behind its personal versioned API.
import $ from "jsr:@deno/shell@1";
await $`echo 'Hiya there!'`;
Let me know if there is a need for a much less practical, extra light-weight model
of dax like that and I am going to look into making it occur.
Once more, now you can set up dax through npm set up --save-dev dax-sh
and use it in
Node.js. Learn the documentation right here:
https://github.com/dsherret/dax