diff --git a/README.md b/README.md index 16ca1bc..2dbcaad 100644 --- a/README.md +++ b/README.md @@ -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' }] + })); }; ``` @@ -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 | |-------------------------------------------------|------------------------------------------------------------| @@ -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` to list all arguments as flags (key/value pairs) * `Array>` 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` @@ -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. --- @@ -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:** @@ -253,17 +268,31 @@ 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' + ])' }; ``` @@ -271,11 +300,11 @@ export default nr({ task, script }) => { ### `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 | |-----------------------------------------------|-----------------------------------------------------------| @@ -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 @@ -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:** @@ -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.' }); @@ -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. }); ``` @@ -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` and `post`. If found, these scripts will be run before -and after the matched script, respectively. +for a script named `pre` and `post`. If found, these scripts will +be run before and after the matched script, respectively. > 💡 **Protip** > @@ -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 @@ -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) diff --git a/src/lib/commands.ts b/src/lib/commands.ts index 23bef31..fb03d16 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -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]); @@ -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]);