Skip to content

Commit

Permalink
docs: Update README.
Browse files Browse the repository at this point in the history
  • Loading branch information
darkobits committed Jan 8, 2024
1 parent 111a799 commit fbf4886
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 74 deletions.
173 changes: 101 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,56 +112,63 @@ A configuration file is responsible for creating **commands**, **tasks**, and **
- **Tasks** are JavaScript functions that may execute arbitrary code. They may be synchronous or
asynchronous. Tasks can be used to interface with another program's Node API, or to perform any
in-process work that does not rely on the invocation of an external CLI.
- **Scripts** contain a set of instructions that are made of up commands, tasks, and other scripts.
These instructions may be run in serial, in parallel, or a combination of both.
- **Scripts** describe a set of instructions composed of commands, tasks, and other scripts. These
instructions may be run in serial, in parallel, or a combination of both.

A configuration file must default-export a function that will be passed a context object that contains
the following keys:

| Key | Type | Description |
|-----------------------|------------|----------------------------------------------------------------------------|
| [`command`](#command) | `function` | Create a new command. |
| [`task`](#task) | `function` | Create a new task. |
| [`script`](#script) | `function` | Create a new script. |
| `isCI` | `boolean` | `true` in CI environments. See [`is-ci`](https://github.com/watson/is-ci). |
| Key | Type | Description |
|-----------------------|------------|-----------------------|
| [`command`](#command) | `function` | Create a new command. |
| [`task`](#task) | `function` | Create a new task. |
| [`script`](#script) | `function` | Create a new script. |

**Example:**

> `nr.config.ts`
```ts
import nr from '@darkobits/nr';
// Using this helper function is optional, but easily enables type-safety and IntelliSense in your
// configuration file.
import defineConfig from '@darkobits/nr';

export default nr({ command, task, script }) => {
export default defineConfig({ command, task, script }) => {
// The first argument to `command` must be the name of the executable to run.
// The second argument is
const babelCmd = command('babel', {
// Arguments are defined as an array of strings and objects. The below
// will be parsed into `src --out-dir "dist"`.
args: ['src', { outDir: 'dist' }],
// This property is optional, but will be needed if we want to reference
// this command using a string (see below).
name: 'babel',
args: ['src', { outDir: 'dist' }]
name: 'babel'
});

// We can then reference this command in a script in several different ways:
// The first argument to `script` must be the name of the script. We can then
// reference commands in a script in several different ways:

// 1. By reference; use the value returned by command directly:
// 1. By reference: use the value returned by command directly.
script('build', [
babelCmd,
]);

// 2. If the command has defined a name, it can be referenced in a script
// using a string with the prefix 'cmd:'. To reference a task, use 'task:',
// and to reference another script, use 'script:'.
// 2. By name: If the command has defined a name in its options, it can be
// referenced in a script using a string with the prefix 'cmd:'. To reference
// a task, use 'task:', and to reference another script, use 'script:'.
script('build', [
'cmd:babel'
]);

// 3. Because commands, tasks, and scripts can be passed by value, it is
// also possible to define them inline in a script's instructions. This
// approach may be useful when a command will only be used by a single script.
script('build', [
command('babel', {
args: ['src', { outDir: 'dist' }]
}),
]);
// approach may be useful when an instruction will only be used by a single
// script. Furthermore, when a script only contains a single instruction, you
// do not need to wrap that instruction in an array. This allows simple
// scripts to become very terse:
script('build', command('babel', {
args: ['src', { outDir: 'dist' }]
}));
};
```

Expand All @@ -173,11 +180,10 @@ nr build

### `command`

| Parameter | Type | Description |
|-----------------|---------------------------------------------------------|--------------------------------------|
| `executable` | `string` | Name of the executable to run. |
| `options?` | [`CommandOptions`](src/etc/types/CommandOptions.ts) | Optional configuration. |
| `options.args?` | [`CommandArguments`](src/etc/types/CommandArguments.ts) | Arguments to supply to `executable`. |
| Parameter | Type | Description |
|-----------------|-----------------------------------------------------|---------------------------------------|
| `executable` | `string` | Name of the executable to run. |
| `options?` | [`CommandOptions`](src/etc/types/CommandOptions.ts) | Optional arguments and configuration. |

| Return Type | Description |
|-------------------------------------------------|------------------------------------------------------------|
Expand All @@ -189,17 +195,23 @@ to specify any [`CommandArguments`](src/etc/types/CommandArguments.ts) to pass t
the invocation of a command.

To reference a command in a script, use either the return value from `command` directly or a string in
the format `cmd:name`. Note that in order for this latter method to work, the command must have defined
`options.name` when it was created.
the format `cmd:name` where name is the value provided in `options.name`.

Commands are executed using [`execa`](https://github.com/sindresorhus/execa), any all Execa options are
supported here.
Commands are executed using [`execa`](https://github.com/sindresorhus/execa), and `CommandOptions`
supports all valid Execa options.

[`CommandArguments`](src/etc/types/CommandArguments.ts) may take one of four forms:
* `string` for singular positional argument or to list all arguments as a single string
* `Record<string, any>` to list all arguments as flags (key/value pairs)
* `Array<string | Record<string, any>>` to mix positionals and flags.

#### Argument Casing

The vast majority of modern CLIs use kebab-case for named arguments, while idiomatic JavaScript uses
camelCase to define object keys. Therefore, `nr` will by default convert objects keys from camelCase to
kebab-case. However, some CLIs (Such as the TypeScript compiler) use camelCase for named arguments. In
such cases, set the `preserveArgumentCasing` option to `true` in the commands' options.

**Example:**

> `nr.config.ts`
Expand All @@ -225,7 +237,8 @@ export default nr({ command, script }) => {
#### `command.node`

This function has the same signature as `command`. It can be used to execute a Node script using the
current version of Node. This variant uses [`execaNode`](https://github.com/sindresorhus/execa#execanodescriptpath-arguments-options).
current version of Node. This variant uses [`execaNode`](https://github.com/sindresorhus/execa#execanodescriptpath-arguments-options)
and the options argument supports all `execaNode` options.

---

Expand All @@ -242,8 +255,10 @@ current version of Node. This variant uses [`execaNode`](https://github.com/sind

This function accepts a function, [`TaskFn`](src/etc/types/TaskFn.ts), and an optional `options` object.
To reference a task in a script, use either the return value from `task` directly or a string in the
format `task:name`. Note that in order for this latter method to work, the task must have defined
`options.name` when it was created.
format `task:name`.

To reference a task in a script, use either the return value from `task` directly or a string in
the format `task:name` where name is the value provided in `options.name`.

**Example:**

Expand All @@ -253,29 +268,43 @@ format `task:name`. Note that in order for this latter method to work, the task
import nr from '@darkobits/nr';

export default nr({ task, script }) => {
const myAwesomeTask = task(() => {
const helloWorldTask = task(() => {
console.log('Hello world!');
}, {
name: 'myAwesomeTask'
name: 'helloWorld'
});

// If a script only references a single instruction, an array is not needed:
script('foo', myAwesomeTask);
const doneTask = task(() => {
console.log('Done.');
}, {
name: 'done'
});

// Just like commands, tasks may be referenced in a script by value (and thus
// defined inline) or using a string with the prefix 'task:'. The following
// two examples are functionally equivalent:

// This is also functionally equivalent:
script('foo', 'task:myAwesomeTask')'
script('myAwesomeScript', [
helloWorldTask,
doneTask
]);

script('myAwesomeScript', [
'task:helloWorld',
'task:done'
])'
};
```
---
### `script`
| Parameter | Type | Description |
|----------------|-----------------------------------------------------|------------------------------------------------------|
| `name` | `string` | Name of the script. |
| `instructions` | [`InstructionSet`](src/etc/types/InstructionSet.ts) | List of other commands, tasks, or script to execute. |
| `options?` | [`ScriptOptions`](src/etc/types/ScriptOptions.ts) | Script configuration. |
| Parameter | Type | Description |
|----------------|-----------------------------------------------------|-------------------------------------------------------|
| `name` | `string` | Name of the script. |
| `instructions` | [`InstructionSet`](src/etc/types/InstructionSet.ts) | List of other commands, tasks, or scripts to execute. |
| `options?` | [`ScriptOptions`](src/etc/types/ScriptOptions.ts) | Optional script configuration. |
| Return Type | Description |
|-----------------------------------------------|-----------------------------------------------------------|
Expand All @@ -286,6 +315,8 @@ It will register the script using the provided `name` and return a value. To ref
another script, use either the return value from `script` directly or a string in the format
`script:name`.
If a script contains a single instruction, it does not need to be wrapped in an array.
The second argument must be an array of [`Instruction`](src/etc/types/Instruction.ts) which may be:
* A reference to a command by name using a `string` in the format `cmd:name` or by value using the value
Expand All @@ -295,12 +326,11 @@ The second argument must be an array of [`Instruction`](src/etc/types/Instructio
* A reference to another script by name using a `string` in the format `script:name` or by value using
the value returned by `script`.
To indicate that a group of [`Instructions`](src/etc/types/Instruction.ts) should be run in parallel,
wrap them in an array. However, no more than one level of array nesting is allowed. If you need more
complex parallelization, write separate, smaller scripts and compose them.
#### Parallelization
If a script only needs to execute a single instruction, it is not necessary to wrap it in an array; it
may be passed directly as the second argument to `script`.
To indicate that a group of [`Instructions`](src/etc/types/Instruction.ts) should be run in parallel,
wrap them in an an additional array. However, no more than one level of array nesting is allowed. If you
need more complex parallelization, define separate, smaller scripts and compose them.
**Example:**
Expand All @@ -311,30 +341,28 @@ import nr from '@darkobits/nr';

export default nr({ command, task, script }) => {
command('babel', {
name: 'babel',
args: ['src', { outDir: 'dist' }]
args: ['src', { outDir: 'dist' }],
name: 'babel'
});

// If a command only has 1 argument or 1 objects declaring its flags, it need
// not be wrapped in an array.
command('eslint', {
args: 'src', name: 'lint'
args: ['src'],
name: 'lint'
});

// The same is true for scripts that only need to execute a single instruction.
script('test', command('vitest'), {
description: 'Run unit tests with Vitest.'
});

const done = task(() => { console.log('Done!'); });
const doneTask = task(() => { console.log('Done!'); });

script('prepare', [
// Run these commands in parallel.
// 1. Run these two commands in parallel.
['cmd:babel', 'cmd:lint']
// Then, run this script.
// 2. Then, run this script.
'script:test',
// Then, run this task.
done
// 3. Finally, run this task.
doneTask
], {
description: 'Build and lint in parallel, then run unit tests.'
});
Expand Down Expand Up @@ -390,14 +418,14 @@ export default (({ command, task, script }) => {
```
Or, `nr` exports a helper which provides type-safety and IntelliSense without requiring a JSDoc or
explicit type annotation. This method will provide IntelliSense in
explicit type annotation.
> `nr.config.js`
```ts
import nr from '@darkobits/nr';
import defineConfig from '@darkobits/nr';

export default nr(({ command, task, script }) => {
export default defineConfig(({ command, task, script }) => {
// Define configuration here.
});
```
Expand Down Expand Up @@ -437,16 +465,16 @@ query `testscript` would successfully match it.
> 💡 **Protip**
>
> If a provided shorthand matches more than 1 script, `nr` will ask you to disambiguate by providing
> If a provided shorthand matches more than one script, `nr` will ask you to disambiguate by providing
> more characters. What shorthands you will be able to use is therefore dependent on how similarly-named
> your scripts are.
> your project's scripts are.
## Pre and Post Scripts
Like [NPM package scripts](https://docs.npmjs.com/cli/v8/using-npm/scripts#pre--post-scripts), `nr`
supports pre and post scripts. Once a query from the CLI is matched to a specific script, `nr` will look
for a script named `pre<scriptName>` and `post<scriptName>`. If found, these scripts will be run before
and after the matched script, respectively.
for a script named `pre<matchedScriptName>` and `post<matchedScriptName>`. If found, these scripts will
be run before and after the matched script, respectively.
> 💡 **Protip**
>
Expand All @@ -456,8 +484,9 @@ and after the matched script, respectively.
## Discoverability
Discoverability and self-documentation are encouraged with `nr`. While optional, consider leveraging the
`group` and/or `description` options when defining scripts. Thoughtfully organizing your scripts and
documenting what they do can go a long way in reducing friction for new contributors.
`name`, `group`, and `description` options where available when defining commands, tasks, and scripts.
Thoughtfully organizing your scripts and documenting what they do can go a long way in reducing friction
for new contributors.
The `--commands`, `--tasks`, and `--scripts` flags may be passed to list information about all known
entities of that type. If `nr` detects that a command, task, or script was registered from a third-party
Expand All @@ -476,7 +505,7 @@ project's `package.json`:
```
`npm run help` will now print instructions on how to interact with `nr`, what scripts are available, and
(hopefully) what each does. Here's an example:
(hopefully) what each one does. Here's an example:
![package-scripts](https://github.com/darkobits/nr/assets/441546/8f43ee46-ac90-47b6-9ac2-ee4330353fb8)
Expand Down
4 changes: 2 additions & 2 deletions src/lib/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ const executeNodeCommand: CommandExecutor = (options: CommandExecutorOptions) =>
* Creates a `CommandThunk` that executes a command directly using `execa`.
*/
export function command(executable: string, opts: CommandOptions = {}) {
ow(executable, 'first argument', ow.string);
ow(executable, 'executable', ow.string);

// Get the name of the package that defined this command.
const sourcePackage = getPackageNameFromCallsite(callsites()[1]);
Expand All @@ -402,7 +402,7 @@ export function command(executable: string, opts: CommandOptions = {}) {
* See: https://github.com/sindresorhus/execa#execanodescriptpath-arguments-options
*/
command.node = (nodeScript: string, opts: CommandOptionsNode = {}) => {
ow(nodeScript, 'node script', ow.string);
ow(nodeScript, 'nodeScript', ow.string);

// Get the name of the package that defined this command.
const sourcePackage = getPackageNameFromCallsite(callsites()[1]);
Expand Down

0 comments on commit fbf4886

Please sign in to comment.