DEV Community

Kinga
Kinga

Posted on

Rush custom commands: execute custom script with autoinstaller dependencies

When writing 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.

But what will happen if you want to use these dependencies in your script? For example, instead of this:

 "commands": [
    {
      "name": "prettier",
      "commandKind": "global",
      "autoinstallerName": "rush-prettier",
      // This will invoke common/autoinstallers/rush-prettier/node_modules/.bin/pretty-quick
      "shellCommand": "pretty-quick --staged"
    }
Enter fullscreen mode Exit fullscreen mode

you want to execute this:

 "commands": [
    {
      "name": "prettier",
      "commandKind": "global",
      "autoinstallerName": "rush-prettier",
      "shellCommand": "node common/scripts/run-pretty-quick-and-some-other-scripts.js"
    }
Enter fullscreen mode Exit fullscreen mode

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

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

Create the command

common/config/rush/command-line.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"]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Create the script

Create your script in common/scripts folder:

common/scripts/print-arguments.js

var minimist = require('minimist');
var args = minimist(process.argv.slice(2));
Enter fullscreen mode Exit fullscreen mode

Run the command

rush install
rush print-arguments --arg1 "Hello world!"
Enter fullscreen mode Exit fullscreen mode

The error

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

The root cause

According to the documentation:

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

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

//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));

Enter fullscreen mode Exit fullscreen mode

E Voila! Works like a charm =)

Acquiring lock for "common\autoinstallers\rush-minimist" folder...
Autoinstaller folder is already up to date

{ _: [], edit: 'Hello world!' }
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
novaastra profile image
NovaAstra

And I use fnm or vnm to manage the node version, nvmrc cannot take effect on the entire rush directory

Collapse
 
novaastra profile image
NovaAstra

I want to execute rush add to install dependencies for subprojects in the root directory, just like pnpm --filter, or enhance the rush add --to function. Is there a reliable implementation?