npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@haltcase/run

v2.0.1

Published

Flexible, function-based task runner where command line options are props

Downloads

266

Readme

@haltcase/run · npm version license @haltcase/run

Flexible, function-based task runner where command line options are props.

Features

  • ✅ Write task files in TypeScript or plain JavaScript
  • Execa built in for shell command execution
  • ✅ Tasks are "just functions" exported from your script files
  • Zod schema support for stricter task execution

Quick start

Install @haltcase/run:

# pnpm
pnpm add --save-dev @haltcase/run

# npm
npm install --save-dev @haltcase/run

# yarn
yarn add --dev @haltcase/run

Create a file in a scripts folder at the root of your project and export your tasks — they're just functions! ¹

// scripts/tasks.js

export const hello = ({ name }) => {
	console.log(`Hello, ${name}!`);
};

Now you can execute these functions and pass options from the command line:

pnpm hr tasks hello --name World
# → Hello, World!

Task files

Task files are simple JavaScript or TypeScript files located (by default) in the scripts folder within your current working directory. Organize them however you like.

.
└── scripts/
    ├── build.js
    ├── database-tasks.ts
    └── dev.ts

To execute a task within one of these files, pass the name of the file and the name of the task. For example:

pnpm hr database-tasks seed

[!NOTE] It is allowed (although confusing) to have multiple task files with the same base name, however you must supply a file extension to make it unambiguous which file you are referring to.

✖ Found multiple task files with the name 'miscellaneous'
Rename the ambiguous files or specify an extension and try again
	C:\dev\sources\js\skrrt\scripts\miscellaneous.js
	C:\dev\sources\js\skrrt\scripts\miscellaneous.ts
	C:\dev\sources\js\skrrt\scripts\miscellaneous.mjs

[!TIP] If you want *.js task files to use a different module type from your root project, add a package.json to your scripts folder that specifies "type": "module" or "type": "commonjs".

CommonJS support

While TypeScript or ESM are much more highly recommended, you can also write task files in CommonJS. Use the *.cjs file extension or create a package.json file in your scripts folder with "type": "commonjs" and use exports.

// scripts/greetings.cjs

exports.hello = ({ name }) => {
	console.log(`Hello, ${name}!`);
};

Tasks

While tasks can be as simple as an exported function, they're capable of more.

TypeScript types

So far, all tasks have been simple functions. However, you can get type-safety by using the task function from @haltcase/run:

// scripts/hello.ts

import type { ParsedOptions } from "@haltcase/run";
import { task } from "@haltcase/run";

interface HelloProps extends ParsedOptions {
	name: string;
}

export const hello = task<HelloProps>(({ name }) => {
	console.log(`Hello, ${name}!`);
});

Asynchronous

Tasks can return a Promise:

export const getUser = async ({ id }) => {
	const userResponse = await fetch(
		`https://fakerapi.it/api/v2/custom?_quantity=1&id=${id}&email=email&website=website`
	);

	const { data } = await userResponse.json();

	console.log(data[0]);
};

Positional arguments

Aside from named options of the form --option, task functions also receive positional or unnamed arguments as an array of strings in the _ property.

// scripts/greetings.js

export const hello = ({ name, _ }) => {
	console.log(`Hello, ${name}! ${_.join(" ")}`);
};
pnpm hr greetings hello --name World Welcome to the cosmos!
# → Hello, World! Welcome to the cosmos!

Everything following a -- option terminator will be treated as positional:

pnpm hr greetings hello --name World -- --ThisIsNotAnOption
# → Hello, World! --ThisIsNotAnOption

Environment variables

Tasks also receive environment variables as the env property:

// scripts/greetings.ts

export const hello = ({ env, name }) => {
	if (env.GREET_LOUDLY) {
		console.log(`HELLO, ${name.toUpperCase()}! LOUD GREETINGS TO YOU.`);
	} else {
		console.log(`Hello, ${name}. Quiet greetings to you.`);
	}
};

Example using sh syntax to set an environment variable for a command:

pnpm hr greetings hello --name World
# → Hello, World. Quiet greetings to you.

GREET_LOUDLY=true pnpm hr greetings hello --name World
# → HELLO, WORLD! LOUD GREETINGS TO YOU.

You can also use other typical methods for loading an environment, including dotenv-cli:

dotenv -c development -- pnpm hr greetings hello --name World
# → HELLO, WORLD! LOUD GREETINGS TO YOU.

By default, env is a full reference to Node's process.env and is a fairly loose dictionary from string keys to values that are string | undefined. If you want to validate specific environment variables, use task.strict.

Shell execution

Each task additionally receives a second argument, giving you access to shell execution powered by Execa.

export const build = async ({ mode }, { $ }) => {
	const { stdout } = await $`vite build --mode ${mode}`;
	console.log(`stdout = ${stdout}`);
};

See API for full API details.

Task option validation using Zod schemas

Using task.strict, you can supply a Zod schema to move validations out of your task's logic. If you transform the input, your task function's TypeScript types will infer the output type.

Configuration

You can configure @haltcase/run using a haltcase.run.{extension} file in your current working directory.

Configuration is loaded using c12 — see the documentation there for the list of supported configuration locations, file types, and other features.

// haltcase.run.ts
import { HaltcaseRunConfig } from "@haltcase/run";

export default {
	taskDirectory: "./scripts",
	quiet: false
} satisfies HaltcaseRunConfig;

You can also set options in the haltcase.run property of your package.json:

{
	// ...
	"haltcase.run": {
		"taskDirectory": "./scripts",
		"quiet": false
	}
}

TypeScript configuration

You will likely want to create a tsconfig.json file in your scripts folder, for example:

{
	// if you want to extend your root config
	"extends": "../tsconfig.json",
	"compilerOptions": {
		// if you want to allow non-TypeScript task files
		"allowJs": true,
		"noEmit": true
	},
	"include": [
		// feel free to remove extensions here
		"**/*.ts",
		"**/*.mts",
		"**/*.cts",
		"**/*.js",
		"**/*.mjs",
		"**/*.cjs"
	],
	"exclude": ["node_modules"]
}

API

Option parsing

Command line option parsing is done with Node's util.parseArgs, but all options are treated as string types. In other words, all options expect values, meaning the following usage will throw an error:

pnpm hr greetings hello --name

If you want to pass a boolean value for an option, pass true/false and parse it yourself or use task.strict, for example using the Zod schema: z.string().transform(value => value === "true").

task

task is a very light wrapper around a function that provides improved type inference.

task<TOptions>(fn: Task<T>): BrandedTask<T>

Usage:

import type { ParsedOptions } from "@haltcase/run";
import { task } from "@haltcase/run";

// without providing a type parameter
export const defaultOptions = task((options) => {
	console.log(options._); // ok, `_: string[]`
	console.log(options.name); // ok, but `name: unknown` and no hints
});

interface CustomOptions extends ParsedOptions {
	name: string;
}

// providing a type parameter
export const customOptions = task<CustomOptions>((options) => {
	console.log(options.name); // ok, `name: string`
});

[!IMPORTANT] Because all command line arguments are strings, you should only use string as the value type for options with this method. If you want to be stricter about value types by validating or transforming them, implement the parsing yourself or use task.strict with a Zod schema.

Task

The type for a task function, which accepts two arguments: the parsed command line options and a utilities object providing tools for shell execution.

<TOptions = ParsedOptions> = (
	options: TOptions,
	utilities: TaskUtilities
) => unknown;

TaskUtilities

Properties:

  • $ – Runa command using Execa's script mode.
  • command – Run a command using Execa (e.g., shell command or script).
  • exec – Same as command, but inherits the parent process' stdio streams by default.

task.strict

Provide the shape for a Zod schema as the first argument and a task function as the second. The task function will receive the safely parsed and validated output of the Zod schema, and its types will be inferred automatically.

Note that you supply a plain object for the shape rather than a full Zod schema.

task.strict<TShape, TSchema?>(shape: TShape, fn: Task<z.infer<TSchema>>): BrandedTaskStrict<z.infer<TSchema>>

Usage:

// scripts/strict-tasks.ts

import { task } from "@haltcase/run";
import { z } from "zod";

export const printCharacter = task.strict(
	{
		// positional/unnamed arguments
		_: z.array(z.string()),
		// environment variables
		env: z.object({}).passthrough(),
		name: z.string(),
		armorClass: z.coerce.number()
	},
	async ({ name, armorClass }) => {
		console.log(`🎲 ${name}\n🛡️  ${armorClass}`);
	}
);

This also allows you to parse, validate, and safely type the _ property beyond an array of strings and the env property more strictly than a simple reference to Node's process.env. For instance:

// scripts/extra.ts

import { task } from "@haltcase/run";
import { z } from "zod";

export const fun = task.strict(
	{
		_: z.array(z.string()).transform((values) => values.length),
		// note: `z.object` strips unspecified keys by default
		env: z.object({
			SECRET_KEY: z.string().min(8)
		})
	},
	async ({ _, env }) => {
		console.log(`Positionals count (_) = ${_}`);
		console.log(`SECRET_KEY = ${env.SECRET_KEY}`);
	}
);
pnpm hr extra fun because there are more words
# → 5
# → undefined

SECRET_KEY=abcdefgh pnpm hr extra fun because there are more words
# → 5
# → abcdefgh