Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
generated fromdarkobits/ts-template

⚙️ A modern, type-safe task runner for JavaScript projects.

License

NotificationsYou must be signed in to change notification settings

darkobits/nr

Repository files navigation


nr (short fornpm run) is atask runner forJavaScript projects.

It can serve as a replacement for or complement to traditionalNPM package scripts.

Contents

Install

npm install --save-dev @darkobits/nr

This will install the package and create an executable in the local NPM bin path (ie:node_modules/.bin). If your shell isconfigured to add this location to your$PATH,you can invoke the CLI by simply runningnr. Otherwise, you may invoke the CLI by runningnpx nr.

You may installnr globally, but this is highly discouraged; a project that depends onnr shouldenumerate it in itsdevDependencies, guaranteeing version compatibility. And, if your$PATH isconfigured to include$(npm bin), the developer experience is identical to installingnr globally.

Philosophy

tl;dr Modern task runners don't need plugin systems.

When tools like Grunt and Gulp were conceived, it was common to build JavaScript projects by explicitlystreaming source files from one tool to another, each performing some specific modification beforewriting the resulting set of files to an adjacent directory for distribution.

This pattern almost always relied onNode streams, a notoriouslyunwieldy API, resulting in the need for aplugin foreach tool that a task-runner supported, pushing a lot of complexity from build tools up to thedevelopers that used them.

Modern tools like Babel, Webpack, TypeScript, and Vite allow for robust enough configuration that theycan often perform all of these jobs using a single invocation of a (usually well-documented)command-line interface, making plugin systems a superfluous layer of abstraction between the user andthe CLI.

Rather than relying on plugins to interface with tooling,nr provides an API for invoking other CLIs,and a means to formalize these invocations in a JavaScript configuration file.

Configure

nr is configured using a JavaScript configuration file,nr.config.js, or a TypeScript configurationfile,nr.config.ts.nr will search for this file in the directory from which it was invoked, andthen every directory above it until a configuration file is found.

A configuration file is responsible for creatingcommands,functions, andscripts:

  • Commands describe the invocation of a single executable and any arguments provided to it, as wellas any configuration related to how the command will run, such as environment variables and how STDINand STDOUT will be handled.
  • Functions are JavaScript functions that may execute arbitrary code. They may be synchronous orasynchronous. Functions can be used to interface with another application'sNode API,or to perform any in-process work that does not rely on the invocation of an external CLI.
  • Scripts describe a set of instructions composed of commands, functions, and other scripts. Theseinstructions 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 containsthe following keys:

KeyTypeDescription
commandfunctionCreate a new command.
fnfunctionCreate a new function.
scriptfunctionCreate a new script.

Example:

nr.config.ts

exportdefault({ command, fn, script})=>{script('build',[command('babel',{args:['src',{outDir:'dist'}]}),command('eslint',{args:'src'})]);};

We can then invoke thebuild script thusly:

nr build

The next sections detail how to create and compose commands, functions, and scripts.

command

ParameterTypeDescription
executablestringName of the executable to run.
options?CommandOptionsOptional arguments and configuration.
Return TypeDescription
CommandThunkValue that may be provided toscript to run the command.

This function accepts an executable name and an options object. The object'sargs property may be usedto specify anyCommandArguments to pass to the executable.CommandOptions also supports a variety of ways to customize theinvocation of a command.

Commands are executed usingexeca, andCommandOptionssupports all valid Execa options.

To reference a command in a script, use either the return value fromcommand or a string in theformatcmd:name where name is the value provided inoptions.name.

Assuming the typePrimitive refers to the union ofstring | number | boolean,CommandArgumentsmay take one the following three forms:

  • Primitive to pass a singular positional argument or to list all arguments as a `string``
  • Record<string, Primitive> to provide named arguments only
  • Array<Primitive | Record<string, Primitive>> to mix positional and named arguments

Each of these forms is documented in the example below.

Argument Casing

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

Example:

nr.config.ts

importdefineConfigfrom'@darkobits/nr';exportdefaultdefineConfig(({ command})=>{// Using single primitive arguments. These commands will invokecommand('echo',{args:'Hello world!'});// echo "Hello world!"command('sleep',{args:5});// sleep 5// Example using a single object of named arguments and preserving argument// casing.command('tsc',{args:{emitDeclarationOnly:true},preserveArgumentCasing:true});// tsc --emitDeclarationOnly=true// Example using a mix of positional and named arguments.command('eslint',{args:['src',{ext:'.ts,.tsx,.js,.jsx'}]});// eslint src --ext=".ts,.tsx,.js,.jsx"// Execa's default configuration for stdio is 'pipe'. If a command opens an// application that involves interactivity, you'll need to set Execa's stdio// option to 'inherit':command('vitest',{stdio:'inherit'});});

command.node

This function has the same signature ascommand. It can be used to execute a Node script using thecurrent version of Node installed on the system. This variant usesexecaNodeand the options argument supports allexecaNode options.


fn

ParameterTypeDescription
userFnFnFunction to execute.
options?FnOptionsFunction options.
Return TypeDescription
FnThunkValue that may be provided toscript to run the function.

This function accepts a functionuserFn and an optionaloptions object.

To reference a function in a script, use either the return value fromfn directly or a string in theformatfn:name where name is the value provided inoptions.name.

Example:

nr.config.ts

importdefineConfigfrom'@darkobits/nr';exportdefaultdefineConfig(({ fn, script})=>{consthelloWorldFn=fn(()=>{console.log('Hello world!');},{name:'helloWorld'});constdoneFn=fn(()=>{console.log('Done.');},{name:'done'});// Just like commands, functions may be referenced in a script by value (and// thus defined inline) or using a string with the prefix 'fn:'. The following// two examples are therefore equivalent:script('myAwesomeScript',[helloWorldFn,doneFn]);script('myAwesomeScript',['fn:helloWorld','fn:done']);});

script

ParameterTypeDescription
namestringName of the script.
instructionsInstructionInstruction orArray<Instruction>; commands, functions, or other scripts to execute in serial.
options?ScriptOptionsOptional script configuration.
Return TypeDescription
ScriptThunkValue that may be provided toscript to run the script.

This function accepts a name, an instruction set, and an options object,ScriptOptions.It will register the script using the providedname and return a value.

To reference a script in another script, use either the return value fromscript directly or a stringin the formatscript:name.

The second argument must be anInstruction or array of Instructions.For reference, an Instruction may be one of:

  • A reference to a command by name using astring in the formatcmd:name or by value using the valuereturned bycommand.
  • A reference to a function by name using astring in the formatfn:name or by value using the valuereturned byfn.
  • A reference to another script by name using astring in the formatscript:name or by value usingthe value returned byscript.

Parallelization

To indicate that a group ofInstructions should be run in parallel,wrap them in an an additional array. However, no more than one level of array nesting is allowed. If youneed more complex parallelization, define separate, smaller scripts and compose them.

Example:

nr.config.ts

importdefineConfigfrom'@darkobits/nr';exportdefaultdefineConfig(({ command, fn, script})=>{command('babel',{args:['src',{outDir:'dist'}],name:'babel'});command('eslint',{args:['src'],name:'lint'});// This script runs a single command, so its second argument need not be// wrapped in an array.script('test',command('vitest'),{description:'Run unit tests with Vitest.'});constdoneFn=fn(()=>console.log('Done!'));script('prepare',[// 1. Run these two commands in parallel.['cmd:babel','cmd:lint']// 2. Then, run this script.'script:test',// 3. Finally, run this function.doneFn],{description:'Build and lint in parallel, then run unit tests.'});script('test.coverage',command('vitest',{args:['run',{coverage:true}]}),{description:'Run unit tests and generate a coverage report.'});});

Warning

Scripts will deference their instructions after the entire configuration file has been parsed. Thismeans that if a script calls a command via a string token and something downstream re-defines a newcommand with the same name, the script will use the latter implementation of the command. This can bea powerful feature, allowing shareable configurations that users can modify in very specific ways. Ifyou want to ensure that a script always uses a specific version of a command, use the pass-by-valuemethod instead of a string token.


Type-safe Configuration & IntelliSense

For users who want to ensure their configuration file is type-safe, or who want IntelliSense, you mayuse a JSDoc annotation in a JavaScript configuration file:

nr.config.ts

/**@type { import('@darkobits/nr').UserConfigurationExport} */exportdefault({ command, fn, script})=>{};

If using a TypeScript configuration file, you can use thesatisfies operator:

nr.config.ts

importtype{UserConfigurationExport}from'@darkobits/nr';exportdefault(({ command, fn, script})=>{// Define configuration here.})satisfiesUserConfigurationExport;

Or,nr exports a helper which provides type-safety and IntelliSense without requiring a JSDoc orexplicit type annotation.

nr.config.ts

importdefineConfigfrom'@darkobits/nr';exportdefaultdefineConfig(({ command, fn, script})=>{// Define configuration here.});

Use

Once you have created annr.config.(ts|js) file in your project and registered commands, functions,and scripts, you may invoke a registered script using thenr CLI:

nr test.coverage

Or, using a shorthand:

nr t.c

More on using shorthands below.

Script Name Matching

nr supports a matching feature that allows the user to pass a shorthand for the desired script name.Script names may be segmented using a dot, and the matcher will match each segment individually.

For example, if we wanted to execute a script namedbuild.watch, we could use any of the following:

nr build.wnr bu.wanr b.w

Additionally, script name matching is case insensitive, so if we had a script namedtestScript, thequerytestscript would successfully match it.

💡Protip

If a provided shorthand matches more than one script,nr will ask you to disambiguate by providingmore characters. What shorthands you will be able to use is therefore dependent on how similarly-namedyour project's scripts are.

Pre and Post Scripts

LikeNPM package scripts,nrsupports pre and post scripts. Once a query from the CLI is matched to a specific script,nr will lookfor a script namedpre<matchedScriptName> andpost<matchedScriptName>. If found, these scripts willbe run before and after the matched script, respectively.

💡Protip

Because script name matching is case insensitive, a script namedbuild may have pre and post scriptsnamedpreBuild andpostBuild.

Discoverability

Discoverability and self-documentation are encouraged withnr. While optional, consider leveraging thename,group, anddescription options where available when defining commands, functions, andscripts. Thoughtfully organizing your scripts and documenting what they do can go a long way in reducingfriction for new contributors.

The--commands,--functions, and--scripts flags may be passed to list information about all knownentities of that type. Ifnr detects that a command, function, or script was registered from athird-party package, it will indicate the name of the package that created it.

A new contributor to a project may want an overview of available scripts, and may not be familiar withwith thenr CLI. To make this feature easily accessible, consider adding an NPM script to theproject'spackage.json:

{"scripts": {"help":"nr --scripts"  }}

npm run help will now print instructions on how to interact withnr, what scripts are available, and(hopefully) what each one does. Here's an example:

package-scripts

Providing an Explicit Configuration File

To havenr skip searching for a configuration file and use a file at a particular path, pass the--config flag with the path to the configuration file to use.

Prior Art


About

⚙️ A modern, type-safe task runner for JavaScript projects.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp