scriptype / salinger

Ecosystem-free task runner that goes well with npm scripts.
https://scriptype.github.io/salinger/
MIT License
69 stars 2 forks source link
build-automation build-pipelines build-tool build-tools npm-scripts task-runner toolkit

Salinger

Ecosystem-free task runner that goes well with npm scripts.

Travis Status Badge AppVeyor Status Badge Coverage Status Badge Code Climate Badge bitHound Overall Score Badge

Salinger is (almost) just a Promise wrapper around the native fs.exec() calls. And it replaces your favorite task runner (See: Trade-offs).

Easy to step in, easy to step out. No attachment to the glue modules between the runner and the tools.

Salinger walk-through

Contents

What Salinger offers

An example with npm run-scripts and Salinger

Let's say we have some scripts in our package.json:

"scripts": {
  "foo": "someCrazyComplicatedStuff && anotherComplicatedThingGoesRightHere",
  "bar": "aTaskThatRequiresYouToWriteThisLongScriptInOneLine",
  "fooBar": "npm run foo && npm run bar"
}

If we had used Salinger, the package.json would look like this:

"scripts": {
  "foo": "salinger foo",
  "bar": "salinger bar",
  "fooBar": "salinger fooBar"
}

And we would implement chaining logic in scripts/tasks.js:

var run = require('salinger').run

module.exports = {
  foo() {
    return run('crazy_complicated')
      .then(_ => run('another_complicated'))
  },
  bar() {
    return run('my_long_script')
  },
  fooBar() {
    this.foo().then(this.bar)
  }
}

(Normally we wouldn't have to return in tasks if we didn't reuse them.)

Finally, we would have the actual scripts in scripts/tasks/:

crazy_complicated.sh
another_complicated.js
my_long_script.rb

(Yes, they can be written in any scripting language.)

So, what did we do here? We have separated the entry points, the orchestration/chaining part and the actual script contents.

Motivation

After spending some time with npm scripts, problems arise:

As a general note, an ideal task runner should run any tasks we want it to. Not the only tasks that are compliant with its API.

Install

npm install --save-dev salinger

Getting started

We have a simple boilerplate project. It'll surely help to understand better what's going on. Really, check it out.

So, let's start a new project and use Salinger in it.

Initialize an empty project:

mkdir test-project
cd test-project
npm init -y

Make sure you've installed Salinger with this:

npm install --save-dev salinger

Let's have a dependency for our project:

npm install --save-dev http-server

Add a start script in the package.json which forwards to our start task:

"scripts": {
  "start": "salinger start"
}

Next, create a folder named scripts in the root directory of our project. We'll use this folder as the home directory for Salinger-related things. It will eventually look like this:

├─┬ scripts/
│ ├── env.js
│ ├── tasks.js
│ └─┬ tasks/
│   └── server.sh

First, let's create the tasks.js inside the scripts:

var run = require('salinger').run

module.exports = {
  start() {
    run('server')
  }
}

So, we have our start task that npm start will redirect to. It runs a script called server, so let's create it.

Create a folder named tasks inside the scripts. This folder will contain all future script files.

mkdir scripts/tasks

Create server.sh inside this folder, and copy the below code and save:

http-server -p $PORT

Last missing part: the script looks for a PORT environment variable but we didn't pass it.

Create env.js inside the scripts folder:

const PORT = process.env.PORT || '8081'

module.exports = {
  PORT
}

Variables you export from env.js is accessible from all scripts, via process.env.

Let's check what we got:

npm start
# starts an http server at 8081

Now that you can add more tasks, that executes different scripts, and chain them together.

At this point I recommend checking out the Salinger-Basic Boilerplate and reading the docs below to explore the possibilities.

Docs

Salinger.run()

Currently being the only member of Salinger's API, run takes two parameters and returns a Promise:

run('do-things', {
  HELLO: 'world'
})

You can chain run calls just like any other Promise:

run('foo')
  .then(_ => run('bar'))
  .then(_ => run('bam'))

Concurrently executing scripts is a no-brainer:

run('foo')
run('bar')

You can use Promise.all, Promise.race etc.

One interesting pattern would be chaining and reusing the exported Salinger tasks:

// scripts/tasks.js
var run = require('salinger').run

module.exports = {

  lorem() {
    return run('foo')
  },

  ipsum() {
    return run('bar')
  },

  dolor() {
    this.lorem()
      .then(_ => run('grapes', { HERE: 'A_VARIABLE' }))
      .then(this.ipsum)
      .then(_ => run('trek'))
  }
}

Changing the default scripts directory

You can choose to have Salinger-related files in a different folder. If that is the case, just add this config to your package.json:

"config": {
  "salinger-home": "path/to/new-folder"
}

Now, you can move everything to that folder and Salinger will start to work with that path. Just be aware that you may need to fix any paths you set in env.js.

Environment variables

There must be a file named env.js in the salinger-home directory. Values exported from this module will be accessible to all tasks through process.env. A sample env.js may look like this:

var path = require('path')

const SRC = path.join(__dirname, '..', 'src')
const DIST = path.join(__dirname, '..', 'dist')

const PORT = 8080

module.exports = {
  SRC,
  DIST,
  PORT
}

This will extend the process.env during the execution of the scripts.

Also, Salinger's run method takes an optional second parameter which also extends process.env with the provided values. But, these values are available only for this specific run call. Let's say we run a script from a task, like this:

myTask(these, parameters, are, coming, from, CLI) {

  // Maybe do some logic depending on the values of CLI parameters.
  // ...

  run('my-script', {

    // Or inject those parameters as environment variables to a script
    these: these,
    parameters: parameters,
    are: are,
    from: from,
    CLI: CLI,

    // Let's pass a variable that conflicts with an existing key in the env.js
    PORT: 5001

  })

}

And, of course, add an entry point for this task to package.json:

"scripts": {
  "myTask": "salinger myTask hello there from planet earth"
}

Say, this is a production environment and there's already a PORT environment variable independent from all of these. Now when we run npm run myTask, that PORT variable will be overridden by 8080, since it's defined in env.js. And, since we specify that variable again, in the second parameter of run call of 'my-script', it gets overriden again just for this execution of 'my-script'. So, PORT is 5001 for my-script, only for this call.

What's exported from env.js, though, will be accessible from process.env (not persistent) during the execution of all scripts.

Trade-offs

This project doesn't claim to be a full-fledged build solution. It helps bringing some consistency and some freedom to the build scripts of projects, especially to the ones that are formerly written with npm run scripts.

Salinger currently doesn't (and, by nature, probably will never) use virtual-fs or streams, which puts it behind the tools like Gulp, in terms of build performance. If your priority is superior build performance, just use Gulp or whatever suits your needs better.

Salinger is more about freedom. It's ecosystem-free, learning-curve-free, provides freedom to choose betwen the CLI and the API. This freedom comes with little to no abstraction. Therefore, it has little to no performance improvements or optimizations.

Windows support

Salinger may or may not work on cmd.exe. Consider using one of these:

...and everything should be fine.

If you encounter a problem with Salinger on Windows, please see the Windows Issues and open a new one if necessary.

Contributing

See: CONTRIBUTING.md

Credits

Many thanks to Ufuk Sarp Selçok (@ufuksarp) for the project logo.