node.jsclojurescriptnpm-scriptsedn

Run zprint-cli recursively on multiple files in nested directories via NPM scripts


Goal: Use npm scripts to run zprint-clj on each file with the appropriate file extension in a bundle of nested folders.

zprint-clj expects a filename in and a filename out each time it's run. eg: zprint-clj -i <filein.ext> -o <fileout.ext>

I'm having difficulty understanding how to recursively run the command/script on each file with the matching file extension. The tutorials and guides I've found don't seem to deal with libraries that require specific files be typed out with the library command.

I am still new to this process, so I may be overlooking something obvious.


Solution

  • Short answer:

    You're not overlooking something obvious. It's rather a case of the feature that you want simply doesn't exist.

    Unfortunately zprint-clj does not provide an option to recursively process .clj files in a bundle of nested folders. It only accepts one input file per usage.

    Perhaps you should post a request for this feature to be added in the projects GitHub repo here.

    Meanwhile, to meet your requirement you could consider creating a custom node script which utilizes zprint-cli. This script can then be invoked via your npm script.


    Long answer with solution:

    The following demonstrates a solution to fulfill your requirement.

    1. Additional packages

    Firstly you'll need to cd to your project directory and install some additional packages locally via npm (Note: we'll also include references to these packages in the devDependencies section of package.json):

    1. Install cli-glob by running:

      npm i -D cli-glob
      
    2. Next, install shelljs by running:

      npm i -D shelljs
      
    3. And, if you haven't got it installed locally already, install zprint-clj by running:

      npm i -D zprint-clj
      

    2. Custom node script

    Next create a custom node script as follows. Let's name the file recursive-zprint.js and save it to a hidden directory named .scripts in the root of your project folder.

        project
        ├─── .scripts
        │    └─── recursive-zprint.js
        ├─── node_modules
        └─── ...
    

    recursive-zprint.js

    const path = require('path');
    const readline = require('readline');
    const shelljs = require('shelljs');
    
    const TICK = process.platform === 'win32' ? '√' : '✔';
    
    var outputDir = '';
    var verbose = false;
    
    // Setup interface to read glob paths piped to stdin.
    const rl = readline.createInterface({
      input: process.stdin,
      output: null,
      terminal: false
    });
    
    // Read each line from process.stdin
    // (i.e. the filepath for each .clj found via the glob pattern)
    rl.on('line', function (filePath) {
      formatData(filePath);
    });
    
    // Handle the optional `-o` argument for the output dir.
    if (process.argv.indexOf('-o') !== -1) {
      outputDir = process.argv[process.argv.indexOf('-o') + 1];
    }
    
    // Handle the optional `-v` argument for verbose logging.
    if (process.argv.indexOf('-v') !== -1) {
      verbose = true;
    }
    
    /**
     * Gets the path to node_modules/.bin executable.
     * @param {string} command - The executable name
     * @returns {string} The path to the executable.
     */
    function getBin(command) {
      return path.join('node_modules', '.bin', command);
    }
    
    /**
     * Obtains the destination path for where the formated file should be saved.
     * Creates directories, and executes the zprint command . If the `-o` argument
     * is not specified via the npm-script the original file is overwritten.
     *
     * @param {String} srcPath - The path to the source file.
     */
    function formatData(srcPath) {
      const destPath = getDestPath(srcPath);
      makeDirectory(path.dirname(destPath));
      shelljs.exec(getBin('zprint-clj') + ' -i ' + srcPath + ' -o ' + destPath);
    
      // Log formatted filepath to console.
      if (verbose) {
        shelljs.echo('\x1b[32m%s\x1b[0m', TICK, destPath);
      }
    }
    
    /**
     * Obtains destination path for where to save the formatted file. Joins the
     * source path, excluding the root directory, with the given output path.
     *
     * @param {String} srcPath - The path to the source file.
     * @return {String} - The destination file path.
     */
    function getDestPath(srcPath) {
      if (outputDir) {
        const parts = srcPath.split(path.sep);
        parts.shift();
        return path.join(outputDir,  parts.join(path.sep));
      }
      return srcPath;
    }
    
    /**
     * Create a new directory and intermediate directories if necessary.
     * @param {String} dirPath - The path for the directory.
     */
    function makeDirectory(dirPath) {
      if (!shelljs.test('-d', dirPath)) {
        shelljs.mkdir('-p', dirPath);
      }
    }
    

    3. Configuring npm-scripts

    Configure your npm-script as follows. Lets name the script zprint:

    Overwriting the original files...`

    {
      ...
      "scripts": {
        "zprint": "glob \"src/**/*.clj\" | node .scripts/recursive-zprint"
      },
      ...
    }
    

    Note the following:

    1. The script above utilizes cli-glob to obtain the path to each .clj file stored in the src directory many levels deep. You'll need to replace the \"src/**/*.clj\" part with a glob pattern suitable for your project.

    2. The list of files found by the glob pattern are then piped to the custom node script (i.e. recursive-zprint.js).

    3. All original source .clj using this configuration will be overwritten with the formatted data.

    Avoid overwriting the original files...

    To avoid overwriting the original .clj files recursive-zprint.js allows an optional -o argument to be passed via the script that specified the output directory.

    {
      ...
      "scripts": {
        "zprint": "glob \"src/**/*.clj\" | node .scripts/recursive-zprint -o \"path/to/output\""
      },
      ...
    }
    

    Note the following:

    1. This example configuration includes the additonal -o argument followed by the path to where the formatted files will be saved.

    2. If you're going to use the -o argument you'll again need to replace the \"path/to/output\" part with a suitable output path for your project.

    4. Source directory vs resultant directory

    Given the last npm script configuration shown (the one which utilizes the -o argument). Lets assume we have a src directory with files as follows:

        project
        ├─── src
        │   ├─── a.clj
        │   ├─── x
        │   │   ├─── b.clj
        │   │   └─── y
        │   │       └─── c.clj
        │   └─── d.clj
        └─── ...
    

    Afer running npm run zprint, via your CLI, the resultant output will be as follows (Note the original source sub directories are preserved in the resultant output):

        project
        ├─── path
        │   └─── to
        │       └─── output
        │           ├─── a.clj
        │           ├─── x
        │           │   ├─── b.clj
        │           │   └─── y
        │           │       └─── c.clj
        │           └─── d.clj
        ├─── src
        │   └─── ... (same as before)
        └─── ...
    

    5. Verbose logging

    If you want to log to the console the path of each file when it has been formatted successfully you can add the -v optional argument too. For example:

    {
      ...
      "scripts": {
        "zprint": "glob \"src/**/*.clj\" | node .scripts/recursive-zprint -v -o \"path/to/output\""
      },
      ...
    }