A lightweight, TypeScript-native task runner inspired by Turborepo. Define your build pipeline with code, not configuration files.
- π TypeScript-first: Define tasks in TypeScript with full type safety
- π¦ Dependency management: Automatic task dependency resolution and execution
- π Watch mode: File watching with intelligent task re-execution
- β‘ Parallel execution: Run independent tasks concurrently
- π― Persistent tasks: Support for long-running processes (servers, watchers)
- π Interruptible tasks: Graceful handling of task interruption
- π Input/Output tracking: File-based change detection for efficient rebuilds
npm install @hackwaly/task
# or
pnpm add @hackwaly/task
# or
yarn add @hackwaly/task- Create a
taskfile.jsin your project root:
import { configInit } from "@hackwaly/task";
const { defineTask } = configInit(import.meta);
export const build = defineTask({
name: "build",
command: "tsc --build",
inputs: ["src/**/*.ts", "tsconfig.json"],
outputs: ["dist/**/*.js"],
});
export const test = defineTask({
name: "test",
command: "vitest run",
dependsOn: [build],
});
export const dev = defineTask({
name: "dev",
command: "tsc --watch",
persistent: true,
});
export default build;- Run tasks:
# Run a single task
npx task run build
# Run multiple tasks
npx task run build test
# Run with watch mode
npx task run build --watch
# List available tasks
npx task listTasks are defined using the defineTask function with the following options:
interface TaskConfig {
name: string; // Task name (required)
description?: string; // Task description for help text
command?: string | string[] | { program: string; args?: string[] };
env?: Record<string, string>; // Environment variables
cwd?: string; // Working directory (defaults to taskfile location)
inputs?: string[]; // Input file patterns (for change detection)
outputs?: string[]; // Output file patterns
persistent?: boolean; // Whether task runs continuously (like servers)
interruptible?: boolean; // Whether task can be interrupted safely
dependsOn?: TaskDef[]; // Task dependencies
}Commands can be specified in multiple formats:
// String (parsed with shell-like parsing)
command: "tsc --build --verbose"
// Array
command: ["tsc", "--build", "--verbose"]
// Object
command: {
program: "tsc",
args: ["--build", "--verbose"]
}Tasks can depend on other tasks. Dependencies are resolved automatically:
export const generateTypes = defineTask({
name: "generate-types",
command: "generate-types src/schema.json",
outputs: ["src/types.ts"],
});
export const build = defineTask({
name: "build",
command: "tsc --build",
inputs: ["src/**/*.ts"],
dependsOn: [generateTypes], // Runs generateTypes first
});For long-running processes like development servers:
export const server = defineTask({
name: "server",
command: "node server.js",
persistent: true, // Runs continuously
interruptible: true, // Can be stopped gracefully
});Watch mode automatically re-runs tasks when their input files change:
npx task run build --watchFeatures:
- Monitors all input patterns defined in tasks
- Ignores
node_modulesby default - Propagates changes through the dependency graph
- Only reruns tasks that are affected by changes
Run one or more tasks:
# Single task
npx task run build
# Multiple tasks
npx task run lint test build
# With watch mode
npx task run build --watchList all available tasks:
npx task listShows task names and descriptions in a formatted table.
import { configInit } from "@hackwaly/task";
const { defineTask } = configInit(import.meta);
export const lint = defineTask({
name: "lint",
description: "Lint TypeScript files",
command: "eslint src/**/*.ts",
inputs: ["src/**/*.ts", ".eslintrc.json"],
});
export const typecheck = defineTask({
name: "typecheck",
description: "Type check TypeScript files",
command: "tsc --noEmit",
inputs: ["src/**/*.ts", "tsconfig.json"],
});
export const build = defineTask({
name: "build",
description: "Build the project",
command: "tsc --build",
inputs: ["src/**/*.ts", "tsconfig.json"],
outputs: ["dist/**/*.js"],
dependsOn: [lint, typecheck],
});
export const test = defineTask({
name: "test",
description: "Run tests",
command: "vitest run",
dependsOn: [build],
});export const generateSchema = defineTask({
name: "generate-schema",
command: "generate-schema api.yaml",
inputs: ["api.yaml"],
outputs: ["src/generated/schema.ts"],
});
export const dev = defineTask({
name: "dev",
description: "Start development server",
command: "vite dev",
persistent: true,
interruptible: true,
dependsOn: [generateSchema],
});
export const buildWatch = defineTask({
name: "build:watch",
description: "Build in watch mode",
command: "tsc --watch",
persistent: true,
dependsOn: [generateSchema],
});Each package can have its own taskfile.js:
// packages/ui/taskfile.js
import { configInit } from "@hackwaly/task";
const { defineTask } = configInit(import.meta);
export const build = defineTask({
name: "build:ui",
command: "rollup -c",
inputs: ["src/**/*", "rollup.config.js"],
outputs: ["dist/**/*"],
});
// packages/app/taskfile.js
import { build as buildUI } from "../ui/taskfile.js";
export const build = defineTask({
name: "build:app",
command: "vite build",
dependsOn: [buildUI], // Cross-package dependency
});For complex tasks, you can provide custom logic:
export const customTask = defineTask({
name: "custom",
async run(ctx) {
// Custom async logic
console.log("Running custom task...");
// Check if aborted
if (ctx.abort.aborted) {
return;
}
// Your custom logic here
},
inputs: ["src/**/*"],
});MIT