{"id":20542281,"url":"https://github.com/darkobits/nr","last_synced_at":"2025-04-14T09:13:27.414Z","repository":{"id":42474271,"uuid":"385107761","full_name":"darkobits/nr","owner":"darkobits","description":"⚙️ A modern, type-safe task runner for JavaScript projects.","archived":false,"fork":false,"pushed_at":"2025-03-02T22:55:02.000Z","size":9509,"stargazers_count":8,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-14T09:13:08.988Z","etag":null,"topics":["cli","javascript","task-runner"],"latest_commit_sha":null,"homepage":"https://darkobits.gitbook.io/nr","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/darkobits.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-07-12T02:53:14.000Z","updated_at":"2025-03-02T22:55:05.000Z","dependencies_parsed_at":"2024-02-05T12:45:42.697Z","dependency_job_id":"fb56aa6d-605b-4b4f-aed1-fc0ac5ed8a71","html_url":"https://github.com/darkobits/nr","commit_stats":{"total_commits":267,"total_committers":3,"mean_commits":89.0,"dds":"0.19475655430711614","last_synced_commit":"f1c4c1b6b05779d415016f7d7d2b28daa2b6ea1b"},"previous_names":[],"tags_count":149,"template":false,"template_full_name":"darkobits/ts-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkobits%2Fnr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkobits%2Fnr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkobits%2Fnr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkobits%2Fnr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/darkobits","download_url":"https://codeload.github.com/darkobits/nr/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248852182,"owners_count":21171842,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","javascript","task-runner"],"created_at":"2024-11-16T01:30:26.976Z","updated_at":"2025-04-14T09:13:27.385Z","avatar_url":"https://github.com/darkobits.png","language":"TypeScript","readme":"\u003cp align=\"center\"\u003e\n  \u003cpicture\u003e\n    \u003csource\n      media=\"(prefers-color-scheme: dark)\"\n      srcset=\"https://github.com/darkobits/nr/assets/441546/deaf314e-3b55-4bb5-89f7-0991c6300c6c\"\n      width=\"360\"\n    \u003e\n    \u003cimg\n      src=\"https://github.com/darkobits/nr/assets/441546/deaf314e-3b55-4bb5-89f7-0991c6300c6c\"\n      width=\"360\"\n    \u003e\n  \u003c/picture\u003e\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\n  \u003ca\n    href=\"https://www.npmjs.com/package/@darkobits/nr\"\n  \u003e\u003cimg\n    src=\"https://img.shields.io/npm/v/@darkobits/nr.svg?style=flat-square\"\n  \u003e\u003c/a\u003e\n  \u003ca\n    href=\"https://github.com/darkobits/nr/actions?query=workflow%3Aci\"\n  \u003e\u003cimg\n    src=\"https://img.shields.io/github/actions/workflow/status/darkobits/nr/ci.yml?style=flat-square\"\n  \u003e\u003c/a\u003e\n  \u003ca\n    href=\"https://depfu.com/repos/github/darkobits/nr\"\n  \u003e\u003cimg\n    src=\"https://img.shields.io/depfu/darkobits/nr?style=flat-square\"\n  \u003e\u003c/a\u003e\n  \u003ca\n    href=\"https://conventionalcommits.org\"\n  \u003e\u003cimg\n    src=\"https://img.shields.io/static/v1?label=commits\u0026message=conventional\u0026style=flat-square\u0026color=398AFB\"\n  \u003e\u003c/a\u003e\n  \u003ca\n    href=\"https://firstdonoharm.dev\"\n  \u003e\u003cimg\n    src=\"https://img.shields.io/static/v1?label=license\u0026message=hippocratic\u0026style=flat-square\u0026color=753065\"\n  \u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cbr /\u003e\n\n`nr` (short for [`npm run`](https://docs.npmjs.com/cli/v7/commands/npm-run-script)) is a\n[task runner](https://www.smashingmagazine.com/2016/06/harness-machines-productive-task-runners/) for\nJavaScript projects.\n\nIt can serve as a replacement for or complement to traditional [NPM package scripts](https://docs.npmjs.com/cli/v7/using-npm/scripts).\n\n## Contents\n\n- [Install](#install)\n- [Philosophy](#philosophy)\n- [Configure](#configure)\n  - [`command`](#command)\n  - [`fn`](#fn)\n  - [`script`](#script)\n  - [Type-safe Configuration \u0026 IntelliSense](#type-safe-configuration--intellisense)\n- [Use](#use)\n  - [Script Name Matching](#script-name-matching)\n  - [Pre and Post Scripts](#pre-and-post-scripts)\n  - [Discoverability](#discoverability)\n  - [Providing an Explicit Configuration File](#providing-an-explicit-configuration-file)\n- [Prior Art](#prior-art)\n\n# Install\n\n```\nnpm install --save-dev @darkobits/nr\n```\n\nThis will install the package and create an executable in the local NPM bin path (ie:\n`node_modules/.bin`). If your shell is [configured to add this location to your `$PATH`](https://gist.github.com/darkobits/ffaee26c0f322ce9e1a8b9b65697701d),\nyou can invoke the CLI by simply running `nr`. Otherwise, you may invoke the CLI by running `npx nr`.\n\nYou may install `nr` globally, but this is highly discouraged; a project that depends on `nr` should\nenumerate it in its `devDependencies`, guaranteeing version compatibility. And, if your `$PATH` is\nconfigured to include `$(npm bin)`, the developer experience is identical to installing `nr` globally.\n\n# Philosophy\n\n\u003e _tl;dr Modern task runners don't need plugin systems._\n\nWhen tools like Grunt and Gulp were conceived, it was common to build JavaScript projects by explicitly\nstreaming source files from one tool to another, each performing some specific modification before\nwriting the resulting set of files to an adjacent directory for distribution.\n\nThis pattern almost always relied on [Node streams](https://nodejs.org/api/stream.html), a notoriously\nunwieldy API, resulting in the need for a [plugin](https://www.npmjs.com/search?q=gulp%20plugin) for\neach tool that a task-runner supported, pushing a lot of complexity from build tools up to the\ndevelopers that used them.\n\nModern tools like Babel, Webpack, TypeScript, and Vite allow for robust enough configuration that they\ncan often perform all of these jobs using a single invocation of a (usually well-documented)\ncommand-line interface, making plugin systems a superfluous layer of abstraction between the user and\nthe CLI.\n\nRather than relying on plugins to interface with tooling, `nr` provides an API for invoking other CLIs,\nand a means to formalize these invocations in a JavaScript configuration file.\n\n# Configure\n\n`nr` is configured using a JavaScript configuration file, `nr.config.js`, or a TypeScript configuration\nfile, `nr.config.ts`. `nr` will search for this file in the directory from which it was invoked, and\nthen every directory above it until a configuration file is found.\n\nA configuration file is responsible for creating **commands**, **functions**, and **scripts**:\n\n- **Commands** describe the invocation of a single executable and any arguments provided to it, as well\n  as any configuration related to how the command will run, such as environment variables and how STDIN\n  and STDOUT will be handled.\n- **Functions** are JavaScript functions that may execute arbitrary code. They may be synchronous or\n  asynchronous. Functions can be used to interface with another application's [Node API](https://vitejs.dev/guide/api-javascript),\n  or to perform any in-process work that does not rely on the invocation of an external CLI.\n- **Scripts** describe a set of instructions composed of commands, functions, and other scripts. These\n  instructions may be run in serial, in parallel, or a combination of both.\n\nA configuration file must default-export a function that will be passed a context object that contains\nthe following keys:\n\n| Key                   | Type       | Description            |\n|-----------------------|------------|------------------------|\n| [`command`](#command) | `function` | Create a new command.  |\n| [`fn`](#fn)           | `function` | Create a new function. |\n| [`script`](#script)   | `function` | Create a new script.   |\n\n**Example:**\n\n\u003e `nr.config.ts`\n\n```ts\nexport default ({ command, fn, script }) =\u003e {\n  script('build', [\n    command('babel', { args: ['src', { outDir: 'dist' }] }),\n    command('eslint', { args: 'src' })\n  ]);\n};\n```\n\nWe can then invoke the `build` script thusly:\n\n```\nnr build\n```\n\nThe next sections detail how to create and compose commands, functions, and scripts.\n\n### `command`\n\n| Parameter       | Type                                                | Description                           |\n|-----------------|-----------------------------------------------------|---------------------------------------|\n| `executable`    | `string`                                            | Name of the executable to run.        |\n| `options?`      | [`CommandOptions`](src/etc/types/CommandOptions.ts) | Optional arguments and configuration. |\n\n| Return Type                                     | Description                                                |\n|-------------------------------------------------|------------------------------------------------------------|\n| [`CommandThunk`](src/etc/types/CommandThunk.ts) | Value that may be provided to `script` to run the command. |\n\nThis function accepts an executable name and an options object. The object's `args` property may be used\nto specify any [`CommandArguments`](src/etc/types/CommandOptions.ts) to pass to the executable.\n[`CommandOptions`](src/etc/types/CommandOptions.ts) also supports a variety of ways to customize the\ninvocation of a command.\n\nCommands are executed using [`execa`](https://github.com/sindresorhus/execa), and `CommandOptions`\nsupports all valid Execa options.\n\nTo reference a command in a script, use either the return value from `command` or a string in the\nformat `cmd:name` where name is the value provided in `options.name`.\n\nAssuming the type `Primitive` refers to the union of `string | number | boolean`, [`CommandArguments`](src/etc/types/CommandOptions.ts)\nmay take one the following three forms:\n\n* `Primitive` to pass a singular positional argument or to list all arguments as a `string``\n* `Record\u003cstring, Primitive\u003e` to provide named arguments only\n* `Array\u003cPrimitive | Record\u003cstring, Primitive\u003e\u003e` to mix positional and named arguments\n\nEach of these forms is documented in the example below.\n\n#### Argument Casing\n\nThe vast majority of modern CLIs use kebab-case for named arguments, while idiomatic JavaScript uses\ncamelCase to define object keys. Therefore, `nr` will by default convert objects keys from camelCase to\nkebab-case. However, some CLIs (Such as the TypeScript compiler) use camelCase for named arguments. In\nsuch cases, set the `preserveArgumentCasing` option to `true` in the commands' options.\n\n**Example:**\n\n\u003e `nr.config.ts`\n\n```ts\nimport defineConfig from '@darkobits/nr';\n\nexport default defineConfig(({ command }) =\u003e {\n  // Using single primitive arguments. These commands will invoke\n  command('echo', { args: 'Hello world!' }); // echo \"Hello world!\"\n  command('sleep', { args: 5 }); // sleep 5\n\n  // Example using a single object of named arguments and preserving argument\n  // casing.\n  command('tsc', {\n    args: { emitDeclarationOnly: true },\n    preserveArgumentCasing: true\n  }); // tsc --emitDeclarationOnly=true\n\n  // Example using a mix of positional and named arguments.\n  command('eslint', {\n    args: ['src', { ext: '.ts,.tsx,.js,.jsx' }]\n  }); // eslint src --ext=\".ts,.tsx,.js,.jsx\"\n\n  // Execa's default configuration for stdio is 'pipe'. If a command opens an\n  // application that involves interactivity, you'll need to set Execa's stdio\n  // option to 'inherit':\n  command('vitest', {\n    stdio: 'inherit'\n  });\n});\n```\n\n#### `command.node`\n\nThis function has the same signature as `command`. It can be used to execute a Node script using the\ncurrent version of Node installed on the system. This variant uses [`execaNode`](https://github.com/sindresorhus/execa#execanodescriptpath-arguments-options)\nand the options argument supports all `execaNode` options.\n\n---\n\n### `fn`\n\n| Parameter  | Type                                      | Description          |\n|------------|-------------------------------------------|----------------------|\n| `userFn`   | [`Fn`](src/etc/types/Fn.ts)               | Function to execute. |\n| `options?` | [`FnOptions`](src/etc/types/FnOptions.ts) | Function options.    |\n\n| Return Type                           | Description                                                 |\n|---------------------------------------|-------------------------------------------------------------|\n| [`FnThunk`](src/etc/types/FnThunk.ts) | Value that may be provided to `script` to run the function. |\n\nThis function accepts a function `userFn` and an optional `options` object.\n\nTo reference a function in a script, use either the return value from `fn` directly or a string in the\nformat `fn:name` where name is the value provided in `options.name`.\n\n**Example:**\n\n\u003e `nr.config.ts`\n\n```ts\nimport defineConfig from '@darkobits/nr';\n\nexport default defineConfig(({ fn, script }) =\u003e {\n  const helloWorldFn = fn(() =\u003e {\n    console.log('Hello world!');\n  }, {\n    name: 'helloWorld'\n  });\n\n  const doneFn = fn(() =\u003e {\n    console.log('Done.');\n  }, {\n    name: 'done'\n  });\n\n  // Just like commands, functions may be referenced in a script by value (and\n  // thus defined inline) or using a string with the prefix 'fn:'. The following\n  // two examples are therefore equivalent:\n\n  script('myAwesomeScript', [\n    helloWorldFn,\n    doneFn\n  ]);\n\n  script('myAwesomeScript', [\n    'fn:helloWorld',\n    'fn:done'\n  ]);\n});\n```\n\n---\n\n### `script`\n\n| Parameter      | Type                                              | Description                                                                                        |\n|----------------|---------------------------------------------------|----------------------------------------------------------------------------------------------------|\n| `name`         | `string`                                          | Name of the script.                                                                                |\n| `instructions` | [`Instruction`](src/etc/types/Instruction.ts)     | `Instruction` or `Array\u003cInstruction\u003e`; commands, functions, or other scripts to execute in serial. |\n| `options?`     | [`ScriptOptions`](src/etc/types/ScriptOptions.ts) | Optional script configuration.                                                                     |\n\n| Return Type                                   | Description                                               |\n|-----------------------------------------------|-----------------------------------------------------------|\n| [`ScriptThunk`](src/etc/types/ScriptThunk.ts) | Value that may be provided to `script` to run the script. |\n\nThis function accepts a name, an instruction set, and an options object, [`ScriptOptions`](src/etc/types/ScriptOptions.ts).\nIt will register the script using the provided `name` and return a value.\n\nTo reference a script in another script, use either the return value from `script` directly or a string\nin the format `script:name`.\n\nThe second argument must be an [`Instruction`](src/etc/types/Instruction.ts) or array of Instructions.\nFor reference, an Instruction may be one of:\n\n* A reference to a command by name using a `string` in the format `cmd:name` or by value using the value\n  returned by `command`.\n* A reference to a function by name using a `string` in the format `fn:name` or by value using the value\n  returned by `fn`.\n* A reference to another script by name using a `string` in the format `script:name` or by value using\n  the value returned by `script`.\n\n#### Parallelization\n\nTo indicate that a group of [`Instructions`](src/etc/types/Instruction.ts) should be run in parallel,\nwrap them in an an additional array. However, no more than one level of array nesting is allowed. If you\nneed more complex parallelization, define separate, smaller scripts and compose them.\n\n**Example:**\n\n\u003e `nr.config.ts`\n\n```ts\nimport defineConfig from '@darkobits/nr';\n\nexport default defineConfig(({ command, fn, script }) =\u003e {\n  command('babel', {\n    args: ['src', { outDir: 'dist' }],\n    name: 'babel'\n  });\n\n  command('eslint', {\n    args: ['src'],\n    name: 'lint'\n  });\n\n  // This script runs a single command, so its second argument need not be\n  // wrapped in an array.\n  script('test', command('vitest'), {\n    description: 'Run unit tests with Vitest.'\n  });\n\n  const doneFn = fn(() =\u003e console.log('Done!'));\n\n  script('prepare', [\n    // 1. Run these two commands in parallel.\n    ['cmd:babel', 'cmd:lint']\n    // 2. Then, run this script.\n    'script:test',\n    // 3. Finally, run this function.\n    doneFn\n  ], {\n    description: 'Build and lint in parallel, then run unit tests.'\n  });\n\n  script('test.coverage', command('vitest', {\n    args: ['run', { coverage: true }]\n  }), {\n    description: 'Run unit tests and generate a coverage report.'\n  });\n});\n```\n\n\u003e **Warning**\n\u003e\n\u003e Scripts will deference their instructions after the entire configuration file has been parsed. This\n\u003e means that if a script calls a command via a string token and something downstream re-defines a new\n\u003e command with the same name, the script will use the latter implementation of the command. This can be\n\u003e a powerful feature, allowing shareable configurations that users can modify in very specific ways. If\n\u003e you want to ensure that a script always uses a specific version of a command, use the pass-by-value\n\u003e method instead of a string token.\n\n---\n\n## Type-safe Configuration \u0026 IntelliSense\n\nFor users who want to ensure their configuration file is type-safe, or who want IntelliSense, you may\nuse a JSDoc annotation in a JavaScript configuration file:\n\n\u003e `nr.config.ts`\n\n```ts\n/** @type { import('@darkobits/nr').UserConfigurationExport } */\nexport default ({ command, fn, script }) =\u003e {\n\n};\n```\n\nIf using a TypeScript configuration file, you can use the `satisfies` operator:\n\n\u003e `nr.config.ts`\n\n```ts\nimport type { UserConfigurationExport } from '@darkobits/nr';\n\nexport default (({ command, fn, script }) =\u003e {\n  // Define configuration here.\n}) satisfies UserConfigurationExport;\n```\n\nOr, `nr` exports a helper which provides type-safety and IntelliSense without requiring a JSDoc or\nexplicit type annotation.\n\n\u003e `nr.config.ts`\n\n```ts\nimport defineConfig from '@darkobits/nr';\n\nexport default defineConfig(({ command, fn, script }) =\u003e {\n  // Define configuration here.\n});\n```\n\n# Use\n\nOnce you have created an `nr.config.(ts|js)` file in your project and registered commands, functions,\nand scripts, you may invoke a registered script using the `nr` CLI:\n\n```\nnr test.coverage\n```\n\nOr, using a shorthand:\n\n```\nnr t.c\n```\n\nMore on using shorthands below.\n\n## Script Name Matching\n\n`nr` supports a matching feature that allows the user to pass a shorthand for the desired script name.\nScript names may be segmented using a dot, and the matcher will match each segment individually.\n\nFor example, if we wanted to execute a script named `build.watch`, we could use any of the following:\n\n```\nnr build.w\nnr bu.wa\nnr b.w\n```\n\nAdditionally, script name matching is case insensitive, so if we had a script named `testScript`, the\nquery `testscript` would successfully match it.\n\n\u003e 💡 **Protip**\n\u003e\n\u003e If a provided shorthand matches more than one script, `nr` will ask you to disambiguate by providing\n\u003e more characters. What shorthands you will be able to use is therefore dependent on how similarly-named\n\u003e your project's scripts are.\n\n## Pre and Post Scripts\n\nLike [NPM package scripts](https://docs.npmjs.com/cli/v8/using-npm/scripts#pre--post-scripts), `nr`\nsupports pre and post scripts. Once a query from the CLI is matched to a specific script, `nr` will look\nfor a script named `pre\u003cmatchedScriptName\u003e` and `post\u003cmatchedScriptName\u003e`. If found, these scripts will\nbe run before and after the matched script, respectively.\n\n\u003e 💡 **Protip**\n\u003e\n\u003e Because script name matching is case insensitive, a script named `build` may have pre and post scripts\n\u003e named `preBuild` and `postBuild`.\n\n## Discoverability\n\nDiscoverability and self-documentation are encouraged with `nr`. While optional, consider leveraging the\n`name`, `group`, and `description` options where available when defining commands, functions, and\nscripts. Thoughtfully organizing your scripts and documenting what they do can go a long way in reducing\nfriction for new contributors.\n\nThe `--commands`, `--functions`, and `--scripts` flags may be passed to list information about all known\nentities of that type. If `nr` detects that a command, function, or script was registered from a\nthird-party package, it will indicate the name of the package that created it.\n\nA new contributor to a project may want an overview of available scripts, and may not be familiar with\nwith the `nr` CLI. To make this feature easily accessible, consider adding an NPM script to the\nproject's `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"help\": \"nr --scripts\"\n  }\n}\n```\n\n`npm run help` will now print instructions on how to interact with `nr`, what scripts are available, and\n(hopefully) what each one does. Here's an example:\n\n![package-scripts](https://github.com/darkobits/nr/assets/441546/8f43ee46-ac90-47b6-9ac2-ee4330353fb8)\n\n## Providing an Explicit Configuration File\n\nTo have `nr` skip searching for a configuration file and use a file at a particular path, pass the\n`--config` flag with the path to the configuration file to use.\n\n# Prior Art\n\n- [NPS](https://github.com/sezna/nps) - `nr` was heavily inspired by [NPS](https://github.com/sezna/nps),\n  originally created by [Kent C. Dodds](https://kentcdodds.com/), which itself was inspired by [a tweet](https://twitter.com/sindresorhus/status/724259780676575232)\n  by [Sindre is a Horse](https://sindresorhus.com/).\n- [`npm-run-all`](https://www.npmjs.com/package/npm-run-all) - The original package scripts\n  parallelization tool.\n- [Grunt](https://github.com/gruntjs/grunt) - _The JavaScript task-runner_.\n- [Gulp](https://github.com/gulpjs/gulp) - _The streaming build system_.\n\n\u003cbr /\u003e\n\u003ca href=\"#top\"\u003e\n  \u003cimg src=\"https://user-images.githubusercontent.com/441546/189774318-67cf3578-f4b4-4dcc-ab5a-c8210fbb6838.png\" style=\"max-width: 100%;\"\u003e\n\u003c/a\u003e\n","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkobits%2Fnr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdarkobits%2Fnr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkobits%2Fnr/lists"}