scriptful
v1.2.1
Published
Better package.json scripts for complex projects
Downloads
36
Readme
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'
export default scripts({
// 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'
export default scripts({
"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'
export default scripts({
"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'
export default scripts({
"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'
export default scripts({
"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.
type CommandOptions = {
run: BaseAction // pass in your command as a string, function or async function
env?: Record<string, string> // environment variables to set
envFile?: string // tell it to read in an environment file
delay?: number // delay in ms before running
cwd?: string // the directory to run the command in
hideLogs?: boolean // hide or show the logs
}
Shorthand string command
import { scripts } from 'scriptful'
export default scripts({
"dev": "echo very simple, clean and easy",
})
Shorthand function command
import { scripts } from 'scriptful'
const myCommand = 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
}
export default scripts({
"dev": myCommand,
})
Full command
import { scripts, command } from 'scriptful'
export default scripts({
"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'
export default scripts({
"fetch": command({
run: async () => {
const response = await fetch("https://example.com")
const data = await response.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'
export default scripts({
"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'
export default scripts({
"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'
export default scripts({
"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'
export default scripts({
"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'
export default scripts({
"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'
export default scripts({
"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'
export default scripts({
"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)