Using npm-scripts has become a popular way of maintaining the various build tasks needed to develop Node.js modules. People like npm-scripts because it's simple! This is a common refrain:
Don't bother with grunt, gulp, or broccoli, just add a little script to your package.json and run it with
npm run name:of:script
Indeed, this is much simpler, but it can quickly become a mess. Take a look at what happened to our testdouble.js library's package.json. Using npm-scripts for everything is simple to start with, but it can't hope to guard against the complexity that naturally accumulates over the life of a project.
We wrote scripty to help us extract our npm scripts—particularly the gnarly ones—into their own files without changing the command we use to run them. To see how to do this yourself, read on!
$ npm install --save-dev scripty
- From your module's root, create a
scripts
directory - If you want to define an npm script named "foo:bar", write an executable
file at
scripts/foo/bar
- Feel a liberating breeze roll over your knuckles as your script is free to roam within its own file, beyond the stuffy confines of a quote-escaped string inside a pile of JSON
- Declare your
"foo:bar"
script in"scripts"
in yourpackage.json
:
"scripts": {
"foo:bar": "scripty"
}
From this point on, you can run npm run foo:bar
and scripty will use npm's
built-in npm_lifecycle_event
environment variable to look up
scripts/foo/bar
and execute it for you.
This pattern is great for extracting
scripts that are starting to become unwieldy inside your package.json
, while
still explicitly calling out the scripts that your package supports (though
where to take that aspect from here is up for
debate).
Ready to take things to the next level? Check this stuff out:
To pass command-line args when you're running an npm script, set them after
--
and npm will forward them to your script (and scripty will do its part by
forwarding them along).
For example, if you had a script in scripts/echo/hello
:
#!/usr/bin/env sh
echo Hello, "$1"!
Then you can run npm run echo:hello -- WORLD
and see your script print
"Hello, WORLD!"
.
Let's say you have two test tasks in scripts/test/unit
and
scripts/test/integration
:
"scripts": {
"test:unit": "scripty",
"test:integration": "scripty"
}
And you want npm test
to simply run all of them, regardless of order. In that
case, just add a "test"
entry to your package.json
like so:
"scripts": {
"test:unit": "scripty",
"test:integration": "scripty",
"test": "scripty"
}
And from then on, running npm test
will result in scripty running all the
executable files it can find in scripts/test/*
.
Suppose in the example above, it becomes important for us to run our scripts in
a particular order. Or, perhaps, when running npm test
we need to do some other
custom scripting as well. Fear, not!
Without changing the JSON from the previous example:
"scripts": {
"test:unit": "scripty",
"test:integration": "scripty",
"test": "scripty"
}
Defining a script named scripts/test/index
will cause scripty to only run that
index
script, as opposed to globbing for all the scripts it finds in
scripts/test/*
.
If you have a certain command that will match mutiple child scripts (for
instance, if npm run watch
matches scripts/watch/js
and
scripts/watch/css
), then you can tell scripty to run the sub-scripts in
parallel by setting a SCRIPTY_PARALLEL
env variable to 'true'
. This may
be used to similar effect as the
npm-run-all module.
To illustrate, to run a scripty script in parallel, you might:
$ SCRIPTY_PARALLEL=true npm run watch
Or, if that particular script should always be run in parallel, you can set the variable in your package.json:
"scripts": {
"watch": "SCRIPTY_PARALLEL=true scripty"
}
Which will run any sub-scripts in parallel whenever you run npm run watch
.
Finally, if you always want to run scripts in parallel, any option can be
set in your package.json under a "scripty"
entry:
"scripty": {
"parallel": true
}
Windows support is provided by scripty in two ways:
- If everything in your
scripts
directory can be safely executed by Windows, no action is needed (this is only likely if you don't have collaborators on Unix-like platforms) - If your project needs to run scripts in both Windows & Unix, then you may
define a
scripts-win/
directory with a symmetrical set of scripts to whatever Unix scripts might be found inscripts/
To illustrate the above, suppose you have this bash script configured as
"test/unit"
in your package.json file and this bash script defined in
scripts/test/unit
:
#!/usr/bin/env bash
teenytest --helper test/unit-helper.js "lib/**/*.test.js"
In order to add Windows support, you could define scripts-win/test/unit.cmd
with this script:
@ECHO OFF
teenytest --helper test\unit-helper.js "lib\**\*.test.js"
With a configuration like the above, if npm run test:unit
is run from a Unix
platform, the initial bash script in scripts/
will run. If the same CLI
command is run from Windows, however, the batch script in scripts-win/
will be
run.
By default, scripty will search for scripts in scripts/
relative to your
module root (and if you're running windows, it'll check scripts-win/
first).
If you'd like to customize the base directories scripty uses to search for your
scripts, add a "scripty"
object property to your package.json like so:
"scripty": {
"path": "../core/scripts",
"windowsPath": "../core/scripts-win"
}
You can configure either or both of "path"
and "windowsPath"
to custom
locations of your choosing. This may be handy in situations where multiple
projects share the same set of scripts.
You can configure scripty to include certain node modules into its executable
search space. This is beneficial if you would like to create a centralized place
for your scripts and then share them across multiple projects. To include modules
add a "scripty"
object property, modules
, to your package.json like so:
"scripty": {
"modules": ["packageA", "packageB"]
}
Each node module must contain a scripts
directory. Below is an example directory
structure:
root/
scripts/
foo
node_modules/
packageA/
scripts/
foo
bar
packageB/
scripts/
bar
baz
In the above example the resolution of foo
would resolve to root.scripts.foo
. Local scripts
take priority over ones defined in modules. The resolution of bar
would resolve to
root.node_modules.packageA.scripts.bar
as packageA was the first module defined
in the scripty.modules
config.
To perform a dry run of your scripts—something that's handy to check which scripts will run from a particular command without actually executing potentially destructive scripts, you can set an environment variable like so:
$ SCRIPTY_DRY_RUN=true npm run publish:danger:stuff
This will print the path and contents of each script the command would execute in the order they would be executed if you were to run the command normally.
Worth mentioning, like all options this can be set in package.json under a
"scripty"
entry:
"scripty": {
"dryRun": true
}
Scripty is now quieter by default.
The output can be configured to a level of verbose
, info
, warn
, or error
.
Any logs equal to or higher than the setting are shown.
All logs are printed to STDERR (to aid in redirection and piping).
$ SCRIPTY_LOG_LEVEL=verbose npm run publish:danger:stuff
This will print the path and contents of each script the command executes.
If you always want scripty to run your scripts at a certain level,
you can set it in your package.json under a "scripty"
entry:
"scripty": {
"logLevel": "warn"
}
SCRIPTY_SILENT
and SCRIPTY_QUIET
are aliases for SCRIPTY_LOG_LEVEL=silent
SCRIPTY_VERBOSE
is an alias for SCRIPTY_LOG_LEVEL=verbose
(also "silent": true
, etc in package.json#scripty)
SCRIPTY_DRY_RUN=true
implies log level info
Explicit setting from logLevel takes precedence; otherwise, conflicting values between silent/verbose/dryRun will respect the highest level. If no setting is provided, scripty will infer its log level from npm's log level.
-
Is this black magic? - Nope! For once, instilling some convention didn't require any clever metaprogramming, just environment variables npm already sets; try running
printenv
from a script some time! -
Why isn't my script executing? - If your script isn't executing, make sure it's executable! In UNIX, this can be accomplished by running
chmod +x scripts/path/to/my/script
(permissions will also be stored in git) -
How can I expect my users to understand what this does? Documenting your project's use of
scripty
in theREADME
is probably a good idea. Here's some copy pasta if you don't feel like writing it up yourself:MyProject uses
scripty
to organize npm scripts. The scripts are defined in thescripts
directory. Inpackage.json
you'll see the wordscripty
as opposed to the script content you'd expect. For more info, see scripty's GitHub.{{ insert table containing script names and what they do, e.g. this }}
This project follows Test Double's code of conduct for all community interactions, including (but not limited to) one-on-one communications, public posts/comments, code reviews, pull requests, and GitHub issues. If violations occur, Test Double will take any action they deem appropriate for the infraction, up to and including blocking a user from the organization's repositories.