DEV Community

loading...

A simple way to replace npm scripts in Deno

vonheikemen profile image Heiker Updated on ・2 min read

Sooo I tried deno, did the traditional "hello world" and 5 seconds later I felt the pain of not having the npm cli and the package.json file. I can't npm start anymore and it bothers me more than it should. Say whatever you want about the node_module folder and npm, Inc. but npm scripts are the bee's knees.

Anyway, as I was saying, Deno's simplicity just punched me in the face. Now this is usually the part were I search for a third party library to fill the void, but not today. Today I just want to run a couple of commands in a sequence, that's not hard, I can do that with some functions. So I created this file in the root of the project.

// Taskfile.js

function run([name, ...args], tasks) {
  name in tasks 
    ? tasks[name](...args) 
    : console.log(`Task "${name}" not found`);
}

async function exec(args) {
  const proc = await Deno.run({ cmd: args }).status();

  if(proc.success == false) {
    Deno.exit(proc.code);
  }

  return proc;
}

The function run is the one that decides which task is going to be executed, and it will be kind enough to tell you if the task you asked for can't be found. But the interesting part of this story is exec, that's the one that's going to execute the external commands I need using Deno.run. Now we can define our tasks.

run(Deno.args, {
  start() {
    exec(['deno', 'run', './src/mod.ts']);
  },
  async install() {
    await exec(['echo', 'Installing stuff....']);
    // do other things
  },
  echo(str) {
    return exec(['echo', str, "\nThis is javascript, y'all"]);
  },
  async test() {
    // need more power? You got it
    if(/* some condition */) {
       await this.echo('Awesome');
    }

    // do other things
  }
});

You can make this even better if you use a dependency to parse the arguments. Deno has one on their standard library.

This is how you run it.

deno run --allow-run ./Taskfile.js start

Now simplicity strikes again. That is quite a command, no one wants to type that, lucky for us we have the shell on our side. To solve this we can make an alias.

alias deno-task='deno run --allow-run ./Taskfile.js'

On windows using powershell.

Set-Alias -Name deno-task -Value deno run --allow-run ./Taskfile.js

You can even improve this with some shell kung-fu.

alias deno-root-task='deno run --allow-run $(git rev-parse --show-toplevel)/Taskfile.js'

If you are in a folder controlled by git, this command can be used to make sure you execute the Taskfile of the root of the project.

If anyone knows how to make an alias in powershell or cmd.exe, please put it in the comments.

Now we are done, we can use deno-task start to start our application/script or use any other custom commands to automate what we need. There is a bunch of things that this script doesn't do but if npm scripts were enough for you so will this.

Discussion

pic
Editor guide
Collapse
bentoumitech profile image
Khaled Bentoumi

Thanks for the article, I've been also working on a tool to replace npm scripts in Deno.

It uses YAML and currently supports permissions github.com/BentoumiTech/denox/

Collapse
vonheikemen profile image
Heiker Author

Looks really good. I see that it can run a selected file with the permissions, but what about random commands? Is there a plan for that?

Collapse
09wattry profile image
Ryan

I was able to use the code from this article and the denox package to create a similar npm feel. If you pass your script in as a string then split the script on the spaces to get your arguments it becomes much more usable.

If Khaled added a logical check on the config file to check if a file or script is being provided I'm sure we could achieve the desired outcome.

async function exec(script: String): Promise<any> {
  const args = script.split(' ');
  const proc = await Deno.run({ cmd: args }).status();

  if(proc.success == false) {
    Deno.exit(proc.code);
  }

  return proc;
}

export default exec;
Collapse
09wattry profile image
Ryan

github.com/BentoumiTech/denox/pull/15 I took the liberty to add the functionality you're looking for. Denox is a pretty awesome package so hopefully, this gets picked up.

Thread Thread
vonheikemen profile image
Heiker Author

That's awesome. But I'm a little bit worried by this right here: const args = script.split(' ');, it doesn't handle quoted arguments. If you give it something like this

some-cli-tool -m "a message" -m "some other message"

You would get.

["some-cli-tool" "-m", "\"a", "message\"", "-m", "\"some", "other", "message\"" ]

Would Deno.run handle that?

Thread Thread
09wattry profile image
Ryan

That's a great observation. Do you have any suggestions on how to handle this case?

Thread Thread
vonheikemen profile image
Heiker Author

I found this package: exec. It does the same thing as that function with more options. You could use that. It only handles double quotes but it's an improvement.

If anyone is curious how they do it, here it is.

function splitCommand(command) {
  var myRegexp = /[^\s"]+|"([^"]*)"/gi;
  var splits = [];

  do {
    //Each call to exec returns the next regex match as an array
    var match = myRegexp.exec(command);
    if (match != null) {
      //Index 1 in the array is the captured group if it exists
      //Index 0 is the matched text, which we use if no captured group exists
      splits.push(match[1] ? match[1] : match[0]);
    }
  } while (match != null);

  return splits;
}
Thread Thread
09wattry profile image
Ryan

What about double quotes enclosed within single quotes, echo 'And he said: "My name is!" ${name}'. It's a contrived example but you get the gist.

Thread Thread
vonheikemen profile image
Heiker Author

For that you would get.

[ "echo", "'And", "he", "said:", "My name is!", "aa'" ]

See, the double quotes stayed, but everything else is broken. Having a bulletproof regex is a real pain. If you want to improve it, you have to modify this var myRegexp = /[^\s"]+|"([^"]*)"/gi;.

Collapse
vintprox profile image
Rodion Borisov

AFAIK, they are going to make denox run as alias to denox run default, so it's even faster to type than denox start. Clear and concise IMO!

Collapse
vintprox profile image
Rodion Borisov

denon, velociraptor, and trex seem promising. I think that they have more chances to be widespread.

Collapse
09wattry profile image
Ryan

I really like the work you've done! It needs an ability to put an "on the fly" script like in package.json

Collapse
44a6z22 profile image
HAMDAOUI Hamza

Thanks for your Article.
Here's a tutorial on how to make an Alias in PowerShell

Collapse
vonheikemen profile image
Heiker Author

Thanks.

I've added the alias example on powershell. I hope I got it right.

Collapse
nepalilab profile image
Nepali Lab

Yeah, so much true. I was learning Deno and the pain of writing flags for each network or dotenv files burns me up. Was looking for a solution. Thank you for the article.

Collapse
patarapolw profile image
Pacharapol Withayasakpunt

I think robo is nice. YAML is so much better than JSON, only inferior to real scripts.

Collapse
vonheikemen profile image
Heiker Author

It does look nice. I've been using task, it also uses YAML but I think it has more features. But really like how robo handle the arguments for the subcommands.

While on the subject of task runners, a few days ago I saw this one: mask. It uses markdown files to define the options of the subcommands. I haven't use it but it looks really cool.