Rush custom commands: execute custom script with autoinstaller dependencies
Tags: rush, node
Published: 2022-02-14 08:09:59

When writing [custom commands](https://rushjs.io/pages/maintainer/custom_commands/) for rush, package dependencies used by your script may be automatically installed using `autoinstaller`.

> Autoinstallers provide a way to manage a set of related dependencies that are used for
scripting scenarios outside of the usual "rush install" context

One example of such a configuration is [Enabling Prettier](https://rushjs.io/pages/maintainer/enabling_prettier/). 

But what will happen if you want to use these dependencies in your script? For example, instead of this:
```json
 "commands": [
    {
      "name": "prettier",
      "commandKind": "global",
      "autoinstallerName": "rush-prettier",
      // This will invoke common/autoinstallers/rush-prettier/node_modules/.bin/pretty-quick
      "shellCommand": "pretty-quick --staged"
    }
```
you want to execute this:
```json
 "commands": [
    {
      "name": "prettier",
      "commandKind": "global",
      "autoinstallerName": "rush-prettier",
      "shellCommand": "node common/scripts/run-pretty-quick-and-some-other-scripts.js"
    }
```

## The command
My new rush command `rush print-arguments` should parse and print arguments provided during the command invocation. The argument parsing is done with `minimist`.

### Create autoinstaller
```powershell
# create the autoinstaller
rush init-autoinstaller --name rush-minimist
# install minimist as a dependency 
cd common/autoinstallers/rush-minimist
pnpm i minimist
# ensure that the common/autoinstallers/rush-minimist/ppnpm-lock.yaml file is up to date
rush update-autoinstaller --name rush-minimist
```

### Create the command

#### common/config/rush/command-line.json
```json
{
  "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json",
  "commands": [
    {
      "name": "print-arguments",
      "commandKind": "global",
      "summary": "Prints provided arguments to the output",
      "autoinstallerName": "rush-minimist",
      "shellCommand": "node common/scripts/print-arguments.js"
    }
  ],
  "parameters": [
    {
      "parameterKind": "string",
      "argumentName": "ARGUMENT1",
      "longName": "--arg1",
      "description": "",
      "associatedCommands": ["print-arguments"]
    }
  ]
}
```

### Create the script
Create your script in common/scripts folder:

### common/scripts/print-arguments.js
```javascript
var minimist = require('minimist');
var args = minimist(process.argv.slice(2));
```

### Run the command

```
rush install
rush print-arguments --arg1 "Hello world!"
```

## The error
```console
Acquiring lock for "common\autoinstallers\rush-minimist" folder...
Autoinstaller folder is already up to date

internal/modules/cjs/loader.js:883
  throw err;
  ^

Error: Cannot find module 'minimist'
Require stack:
- [...]\common\scripts\print-arguments.js
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:880:15)
    at Function.Module._load (internal/modules/cjs/loader.js:725:27)
    at Module.require (internal/modules/cjs/loader.js:952:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    ...
    (internal/modules/run_main.js:72:12) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '[...]\\common\\scripts\\print-arguments.js'
  ]
}

The script failed with exit code 1
```
## The root cause

According to the [documentation](https://rushjs.io/pages/configs/command-line_json/):
> The `autoinstallerName` [...] "my-task" would map to "common/autoinstallers/my-task/package.json", and the "common/autoinstallers/my-task/node_modules/.bin" folder would be added to the shell `PATH`.
   
And indeed, the `minimist` is in `common/autoinstallers/rush-minimist/node_modules`, and `process.env.PATH` does include `common/autoinstallers/my-task/node_modules/.bin`.

What is the issue then? 

When requiring a module without specifying a path, Node will look for it in all the paths specified by `module.paths`:
```
[    
  'C:\\folder1\\folder2\\project\\common\\scripts\\node_modules',
  'C:\\folder1\\folder2\\project\\common\\node_modules',
  'C:\\folder1\\folder2\\project\\node_modules',
  'C:\\folder1\\folder2\\node_modules',
  'C:\\folder1\\node_modules',
  'C:\\node_modules'
]
```
The `common/autoinstallers/my-task/node_modules/` is not on the list and in effect, node is throwing a "cannot find module error."

## The solution
Modules that are outside of the above node_modules directories can be found using either relative or absolute paths. All we need to do is to find it.

### common/scripts/print-arguments.js
```javascript
//1. See current location: this would be {repo}/common/scripts path
//   console.log(__dirname )
//2. Packages installed by autoinstaller are saved to common/autoinstallers/autoinstaller-name/ and added to the shell PATH
//   console.dir(process.env.PATH);
//3. Knowing current location, and location of the node_modules with packages, path will be ../autoinstallers/autoinstaller-name/node_modules/
//  Get node_modules location

const path = require('path');
const node_modules = path.join(__dirname, '..', 'autoinstallers/rush-minimist/node_modules');

var argv = require(path.join(node_modules, 'minimist'))(process.argv.slice(2));

```

E Voila! Works like a charm =)
```console
Acquiring lock for "common\autoinstallers\rush-minimist" folder...
Autoinstaller folder is already up to date

{ _: [], edit: 'Hello world!' }
```