Skip to content

Repository files navigation

Scriptful

Create better package.json scripts

Why?

I love the simplicity of package.json scripts, they are easy to understand and easy to use. But they are limited, they can only run one command at a time, and they can't be reused. This is where Scriptful comes in, it allows you to write your scripts in typescript, and gives you the ability to run multiple commands at once, and reuse them. It also gives you the ability to run scripts in parallel or sequentially, and even conditionally.

Setup

Install the package with one of these

npm i -D scriptful
yarn add -D scriptful
pnpm add -D scriptful
bun add -d scriptful

create a scripts.ts file in the root of your project

import{scripts}from'scriptful'exportdefaultscripts({// your scripts here})

Note: you may need to add scripts.ts to the exclude part of your tsconfig.

Usage

Define a standard script

Yes I know this is boring but we got to start somewhere

import{scripts}from'scriptful'exportdefaultscripts({"dev": "next dev",})

Here we just define that calling scriptful start will run next dev

Define a script with arguments

import{scripts,command}from'scriptful'exportdefaultscripts({"start": command({run: "next dev",env: {PORT: "5000",},},"Runs the Next.js development server"),})

Using the command function we get a couple more options, check out the reference below to see what else you can do. Notice the second argument is a description, this will be used in the help menu (scriptful --help).

Something a little wild

Now lets imagine a build and deployment process, we want to build the database schema, migrate the database, build the website, run the linting and the tests, and lastly deploy it. This can all be done with one command, and your a wizard at your shell and don't want another dependency. But what about everyone else on your team, are they going to understand the && and ; and how they work? No, they are not. So lets make a script for them.

import{scripts,parallel,sequential,variants,command}from'scriptful'exportdefaultscripts({"build": variants({"prod": sequential(["prisma generate","next build",parallel(["next lint","next test",]),parallel([command({run: "prisma migrate deploy",envFile: ".env.production",}),"firebase deploy",])],"Build, Test and Deploy the website"),})})

Now with a simple yarn scriptful build:prod we kick the whole process off.

CLI Commands

scriptful --help

Using your scripts.ts file, it will generate a help menu for you, showing all the scripts and their descriptions.

scriptful --version

Prints out the version of scriptful you are using.

scriptful --generate

Parses in your scripts.ts file and generates the scripts section of the package.jon file for you to copy over.

scriptful <script>

Runs the script you pass in, if it exists.

Functions

Scripts

This is your root level function, default export this for the cli to find it. In here you pass an object of key value pairs, where the key is the name of the script and the value is the script itself.

import{scripts,parallel,command}from'scriptful'exportdefaultscripts({"dev": parallel(["tsc --watch",command({run: "nodemon ./dist/index.js",delay: 1000,// give typescript a second to compile})]),})

Command

This is the your atom if you will, the lowest level we can go. With two options of defining, a shorthand version for simplicity and a full version with all the bells and whistles. The shorthand version is just a string, function or async function. This will be run as is, with no extra options. The full version is an object with the following options.

typeCommandOptions={run: BaseAction// pass in your command as a string, function or async functionenv?: Record<string,string>// environment variables to setenvFile?: string// tell it to read in an environment filedelay?: number// delay in ms before runningcwd?: string// the directory to run the command inhideLogs?: boolean// hide or show the logs}

Shorthand string command

import{scripts}from'scriptful'exportdefaultscripts({"dev": "echo very simple, clean and easy",})

Shorthand function command

import{scripts}from'scriptful'constmyCommand=async()=>{// - start a server// - open a database connection// - read from the file system// - fetch from an api// its a free world, whatever you feel like doing is free game}exportdefaultscripts({"dev": myCommand,})

Full command

import{scripts,command}from'scriptful'exportdefaultscripts({"dev": command({run: "echo many more options",env: {PORT: "5000",},envFile: ".env",delay: 1000,cwd: "./frontend",hideLogs: false,},"Runs the Node.js development server"),})

Full command using function

import{scripts,command}from'scriptful'exportdefaultscripts({"fetch": command({run: async()=>{constresponse=awaitfetch("https://example.com")constdata=awaitresponse.json()console.log(data)},env: {PORT: "5000",},envFile: ".env",delay: 1000,cwd: "./frontend",hideLogs: false,},"Runs the Node.js development server"),})

Conditional

Since we are in the land of code, we get the fun ability to make decisions. This is done with the conditional function. It takes two arguments, the first is a boolean, the second is a command. If the boolean is true, the command will run, if false it will not.

import{conditional,scripts}from'scriptful'exportdefaultscripts({"dev": conditional({run: "echo only run in development",condition: process.env.NODE_ENV==="development",})})

Lifecycle

Sometimes we need to do something over the lifecycle of say a development environment, we need to setup something, and tear down something afterwards. For example a database. The Lifecycle command lets you do just that.

import{command,lifecycle,scripts,sequential}from'scriptful'exportdefaultscripts({"dev": lifecycle({start: sequential(["supabase start",command({run: "prisma db push",envFile: ".env.development"})]),run: "next dev -H 0.0.0.0",stop: "supabase stop"},"Start up the local development environment"),})

Note: stop gets run under two conditions, when the process is killed, and when the run process is finished.

Optional

when building a long sequential flow, some steps you might not want to run all the time. For example, if you are deploying to firebase, you might not want to run the tests. The optional command lets you do just that. The user will be asked yes or no if they want to run the command. If they say yes, it will run, if no, it will skip.

import{command,optional,scripts,sequential}from'scriptful'exportdefaultscripts({"deploy": sequential(["next build",optional("next lint","Lint the codebase"),optional("next test","Run the unit tests"),command({run: "firebase deploy",envFile: ".env.production",})],"Build, Test and Deploy the website"),})

It is highly recommended you define a description for the optional command, otherwise the user will have no idea what the command is. The description is the second argument.

Parallel

Sometimes we want to run multiple commands at the same time. This is where the parallel command comes in. It takes an array of commands and runs them all at the same time.

import{parallel,scripts}from'scriptful'exportdefaultscripts({"dev": parallel(["docker-compose up","nodemon -e graphql --exec graphql-codegen","tsc --noEmit --watch","next dev"]),})

Repeat

If you want to run a function multiple times in a row, here you go.

import{repeat,scripts}from'scriptful'exportdefaultscripts({"dev": repeat({run: "echo hello",times: 5}),})

Sequential

To run multiple commands in a row, use the sequential command. It takes an array of commands and runs them in order.

import{scripts,sequential}from'scriptful'exportdefaultscripts({"build": sequential(["docker-compose up -d","graphql-codegen","tsc --noEmit","next build"]),})

Variants

say you want a "build:debug", "build:local" and "build:prod", use variants function to create them. It takes an object with the variants as keys and the commands as values.

import{lifecycle,optional,scripts,sequential,variants}from'scriptful'exportdefaultscripts({"build": variants({"debug": sequential(["tsc --noEmit","vitest","next dev"]),"local": lifecycle({start: sequential(["docker-compose up -d","prisma generate","prisma db push",]),run: sequential(["next build","next start"]),stop: "docker-compose down"}),"prod": sequential(["prisma generate","prisma migrate deploy","next build","next-sitemap",optional("firebase deploy")]),}),})

Examples in the wild

(feel free to pr to add yours)

About

Create better package.json scripts

Resources

Code of conduct

Contributing

Stars

49 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages