DEV Community

Cover image for Creating dynamic task runners for your npm scripts in Emacs
Rajasegar Chandran
Rajasegar Chandran

Posted on

Creating dynamic task runners for your npm scripts in Emacs

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

        "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"
Enter fullscreen mode Exit fullscreen mode

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

    npm run lint
Enter fullscreen mode Exit fullscreen mode

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.


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

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

    :command (lambda (&rest args)
               (let ((service (plist-get args :service)))
                 ;; ...
Enter fullscreen mode Exit fullscreen mode

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

    (require 'prodigy)
    (defun my/create-prodigy-service (&optional package-manager)
      "Create new prodigy services based on current package.json"
      (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")
                        :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))
Enter fullscreen mode Exit fullscreen mode

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:

Enter fullscreen mode Exit fullscreen mode

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:

    (my/create-prodigy-service "yarn")
Enter fullscreen mode Exit fullscreen mode

For using pnpm:

    (my/create-prodigy-service "pnpm")
Enter fullscreen mode Exit fullscreen mode

Image description

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

Top comments (1)

camdez profile image
Cameron Desautels • Edited

This looks super handy! I don't know how I hadn't heard of this. Thank you for sharing it!