sqript

1.0.2 • Public • Published

github actions codecov

TL;DR

What problem does it solve?

Sqript is an npm package that helps author and execute scripts with JavaScript. Its goal is to make development workflows descriptive, composable, and maintainable.

Motivation

Why it was built?

NPM scripts feature is super convenient, but I always found it difficult to keep my head around the package.json scripts section as development workflows grow in number and complexity. Even when using terrific packages like concurrently, composing scripts came at the cost of readability. See if you agree with me after looking at this personal example below.

an unconformable personal npm scripts example

I've also tried Bash, Gulp, Grunt and ZX. They are all great pieces of battle-tested software (and possibly a better alternative to you) but, somehow, I never felt at home with the API.

I think the point is to have something that offers the power and ergonomics of a modern language without losing the simplicity expected from tools peripheral to the programming challenge itself.

The next sections will show how Sqript works and enable you to decide if it deserves a try.

Quick Start

The first step is to install Sqript as a dev dependency.

npm i -D sqript

You can install it globally if you prefer, perhaps in the case of using it in a mono repo or on a non-node project.

npm i -g sqript

Then create a sqript.config.js file in the root project folder and open it so we can author our first script.

// sqript.config.js
export const lint = {
  command: "eslint . --ext .js",
};

There are two ways we can use Sqript to run the command we just created. The first is to pass the script name as an argument in the command line. Like this:

npx sqript --name=lint

The second way is to call Sqript without any arguments. It will show the list of available scripts. Choose the one you want to run and voilà.

npx sqript

Note that Sqript will also attempt to find a sqript.config.mjs file and we can pass an arbitrary file using the config argument, like so:

npx sqript --name=lint --config=my-scripts.js

Lint commands are essential, but things will be way more fun when we start to compose and style challenging scripts in the next sections.

Composing Scripts

The last section explored the most basic script type supported: command. Nevertheless, Sqript can handle more complex ones to enable scripts orchestration. The next type we will look at is the relay.

Relay

Let's explore a scenario where we want to pass some quality checks and if, everything goes well, deploy to production. We could achieve that with a relay.

export const test = {
  command: "jest",
};

export const compile = {
  relay: [{ command: "rimraf dist/*" }, { command: "tsc" }],
};

export const publish = {
  command: "some-host-cli publish",
};

export const deploy = {
  relay: [test, compile, publish],
};

One thing to observe here is the role of the export statement. Every script exported in sqript.config.js will be available for execution. In the last example, not only does deploy take advantage of compile, but we could execute compile independently with npx sqript --name=compile.

Coming back to the relay subject, relays attempt to run their scripts sequentially but will stop immediately if any of them fail. This is useful if someone wants to cancel the workflow when lint or test commands fail, for example.

Another sequential type available is the serial script.

Serial

Serial runs all its scripts one after another; but, contrary to relay, will keep going even if one of its child scripts fails.

// ... base scripts definition

export const diagnoses = {
  serial: [lint, unitTests, integrationTests, e2eTests],
};

Serial can be useful to run a bunch of scripts we want to collect output from, independent of whether they fail or not.

We explored the two sequential types: relay and serial, but Sqript can also orchestrate using parallel approaches with rally and race.

Rally

Rally will start all its children as early as possible and keep running until everyone exits, no matter if with failure or success.

export const servers = {
  rally: [testsWatchMode, localWebServer, cloudEnvEmulator],
};

Compositions with rally could be useful to kickstart an ongoing local dev environment dependent on different components. But if we need an exit strategy, the race alternative comes to the rescue.

Race

Like its rally cousin, race will start every child script as soon as possible. But when any of them completes (no matter if with success or failure), race will kill all the others and exit.

const testCiWorkflow = {
  race: [servers, testCi],
};

export const deploy = {
  relay: [testCiWorkflow, publish],
};

In the above example, we can see that different script types can be put together to achieve challenging workflows.

The capability of putting together scripts with different execution rules is the bulk of what Sqript offers. Even so, there are still some complementary features worth discussing.

Styling Prefixes

Sqript will print every command output in the terminal with prefixes to enable disambiguation. But as scripts become more complex, especially in parallel modes, things can be hard to disambiguate. Fortunately, we can improve the situation with some prefixes customization.

The first thing to know is that the prefix is based on the name property of the script. Note below:

export const publishFromLocalhost = {
  name: "publish-local",
  relay: [
    lint,
    testDev,
    {
      name: "patch",
      command: "npm version patch",
    },
    publish,
  ],
};

If name is omitted, Sqript will use the kebab-case version of the variable name for exported scripts. All the others will receive a random string as a prefix.

Another way to control prefixes is to pass a length argument to Sqript. Prefixes will obey the given value by padding or truncating names. Like this:

npx sqript --length=5 --name=test-workflow

Finally, every script can have a styles property to set the prefix appearance. The styles property is an array of strings supported by the great Chalk package.

export const baseTestWorkflow = {
  name: "test-workflow",
  styles: ["underline", "blue"],
  race: [
    {
      name: "server",
      styles: ["red"],
      command: "webpack serve --config webpack.config.js",
    },
    {
      name: "test",
      styles: ["bgGreenBright", "whiteBright"],
      command: "jest --coverage=false",
    },
  ],
};

Please don't judge my aesthetic choices.

Commands Environment Variables

Another supported feature is the ability to pass environment variables to commands. The env property can be a string with the path to some .env file or an object with string values to be injected in the command.

const serverTemplate = (type) => ({
  name: `server-${type}`,
  command: "webpack serve",
  styles: ["bgWhite", "yellow"],
});

export const serverLocal = {
  ...serverTemplate("local"),
  env: ".env",
};

export const serverCi = {
  ...serverTemplate("ci"),
  env: {
    SERVER_PORT: "8081",
  },
};

The last example creates scripts variations taking advantage of a function and the spread operator. Although naive, it highlights a promising potential available to every Sqript config file.

Lastly, you can also mix environment configurations using an array.

export const localDev = {
  rally: [
    firebaseEmulators
    webpackDevServer,
  ],
  env: [
    '.credentials.env',
    {
      APP_ENV_MODE: "DEVELOPMENT",
      APP_ENV_FIREAUTH_EMULATOR_HOST: "http://localhost:9099",
      APP_ENV_FIRESTORE_EMULATOR_HOST: "8080",
    },
  ],
};

Passing Arguments Downstream

Finally, you maybe want to pass arbitrary arguments to a command that will vary from execution to execution. We can let that opportunity open by setting the args property to true. Like this:

export const devTests = {
  styles: ["bgGreenBright", "whiteBright"],
  command: "jest",
  args: true,
};

Now, we can execute Sqript with the last example passing a test name.

npx sqript --name=dev-tests --length=5 my_spec_name

We can pass as many arguments as wanted, but they all need to be anonymous (not preceded by "-" or "--"). The command executes normally if no anonymous argument is provided.

Contribution

How can I help?

Bug reports and feature requests are welcome in the form of issues. However, I decided to keep the project closed to pull requests contributions.

Wrapping up

What to expect?

The project has limited ambitions. The current set of features and their API can be considered stable; from now on, I expect only to deal with the undoubtedly coming bugs.

If you want to talk, feel free to contact me via social media.

🏜️ Fear is the mind-killer.

License

Made by João Melo and licensed under the GNU General Public License v3.0 — see the LICENSE file for details.

Package Sidebar

Install

npm i sqript

Weekly Downloads

63

Version

1.0.2

License

GPL-3.0

Unpacked Size

56.1 kB

Total Files

15

Last publish

Collaborators

  • joaomelo