Creating dynamic task runners for your npm scripts in Emacs
Tags: productivity, emacs, programming
Published: 2024-01-03 08:58:05



In this post, I will show you how to create dynamic task runners for your npm projects in Emacs. This is a simple example of how to use Emacs We are going to use prodigy for this example

Usually inside an npm project we run the tasks from the package.json file from the `scripts` section. A typical package.json file will look like the following

```json
    {
        "name": "alacritty-themes",
        "version": "5.3.1",
        "description": "Themes for Alacritty : A cross-platform GPU-Accelerated Terminal emulator",
        "main": "index.js",
        "bin": {
            "alacritty-themes": "./bin/cli.js"
        },
        "scripts": {
            "commit": "cz",
            "deploy": "git push && git push --tags && npm publish",
            "lint": "eslint .",
            "semantic-release": "semantic-release",
            "test": "mocha --recursive"
        }
    }
```

So if you want to run the lint task you will run the following in your shell:

```shell
    npm run lint
```

If you are inside Emacs, you probably need to open a new terminal or `eshell` buffer and manually type the above command. Also if want to run another command in parallel
you need to open a separate buffer and run the command.

So this involves a lot of manual work and it is not a very efficient way to do it. What if we can create a task runner that will run the tasks in parallel? What if we can do that in a way that will be dynamic? That is exactly what we are going to do here using a prodigy service.


## Prodigy

What is prodigy? Prodigy is a framework for creating Emacs-based task runners. You can use it to manage external services from within Emacs. You can find more details in the  [prodigy documentation](https://github.com/rejeep/prodigy.el) 

To create new prodigy services you need to call the `prodigy-define-service` function

```elisp
    (prodigy-define-service
    :command (lambda (&rest args)
               (let ((service (plist-get args :service)))
                 ;; ...
                 )))
```

Now, let us see how we can achieve this using prodigy in Emacs

```elisp
    (require 'prodigy)
    (defun my/create-prodigy-service (&optional package-manager)
      "Create new prodigy services based on current package.json"
      (interactive)
      (let ((pkg (json-parse-string (buffer-substring-no-properties (point-min) (point-max)))))
        (maphash  (lambda (key value)
                    (let ((args '())
                          (name (gethash "name" pkg)))
                      (add-to-list 'args key)
                      (add-to-list 'args "run")
                      (prodigy-define-service
                        :name (concat name "-" key)
                        :command (or package-manager "npm")
                        :cwd (file-name-directory (buffer-file-name))
                        :path (file-name-directory (buffer-file-name))
                        :args args
                        :tags '(temp)
                        :stop-signal 'sigkill
                        :kill-process-buffer-on-stop t
                        ))) (gethash "scripts" pkg))
        (prodigy)
        (prodigy-refresh)))
```

This code is defining a function called `my/create-prodigy-service` that creates new prodigy services based on the contents of the current `package.json` file.
You can put this in your init.el file and call it from within Emacs.

Here is a breakdown of the code:

The `(interactive)` line is important if you plan to use the function interactively, meaning you can call it through a keybinding or by typing `M-x` and the function name.

In this line, the contents of the current buffer are parsed as a JSON string using the `json-parse-string` function.
The result is stored in a variable called `pkg`. The `(point-min)` and `(point-max)` functions are used to get the positions of the first and last characters of the buffer, so that `buffer-substring-no-properties` can extract the contents of the buffer as a string.

Then we use the `maphash` function to iterate over each key-value pair in the `"scripts"` hash in the `pkg` variable. The `gethash` function is used to get the value associated with the `"scripts"` key.

And next we define an anonymous function that takes two arguments: `key` and `value`. This function will be used as the mapping function for `maphash`.

Inside the mapping function, a new variable `args` is initialized as an empty list. Another variable `name` is assigned the value of the `"name"` key in the `pkg` variable.

Then we define a new prodigy service using the `prodigy-define-service` function. The `:name` parameter uses the `concat` function to combine the `name` and `key` variables. The `:command` parameter is set to "npm". The `:cwd` and `:path` parameters are set to the directory of the current buffer file. The `:args` parameter is set to the `args` list. The `:tags` parameter is set to '(temp), indicating that the service is temporary. The `:stop-signal` parameter is set to `sigkill` to force-stop the service. The `:kill-process-buffer-on-stop` parameter is set to `t` to automatically kill the process buffer when the service is stopped.

Finally, `prodigy` is called to start all defined prodigy services and then `prodigy-refresh` to refresh the list of services.


Overall, this code creates prodigy services for each script defined in the `"scripts"` section of the `package.json` file. It uses the name of the package as part of the service name, and sets the appropriate command, working directory, and arguments for each service.

Here is how it works, open your package.json file situated in the project root folder.
And invoke `M-x my/create-prodigy-service`

If you want to call inside a lisp file, you can define a function and call it using the following code:

```elisp
    (my/create-prodigy-service)
```

By default it will use `npm` as the default package manager, if you want to use any other package  manager, you can specify it as follows:

For using yarn:

```elisp
    (my/create-prodigy-service "yarn")
```

For using pnpm:

```elisp
    (my/create-prodigy-service "pnpm")
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b0bc1w2n0iyv0qtl6cxr.png)

Hope you enjoyed the post, if you have any alternate approaches, feedback or queries, please let me know in the comments section.