skier. v1.0.0

Task Contract

Everything Skier runs — every built-in and every custom task — is a TaskDef. This page is the reference for that contract: the shape you implement, the context you're handed, and the config-in / globals-out rule run() follows.

The signatures below are transcluded straight from src/types.ts, so they are exactly what the package exports.


TaskDef

A task is a plain object with a name, its config, and an async run function. It's generic over its config and output types:

export interface TaskDef<Config = unknown, Output = unknown> {
  name: string;
  title?: string;
  config: Config;
  run: (config: Config, ctx: TaskContext) => Promise<Output>;
}
Field Type Required Purpose
name string Unique identifier. Used in logs and by the CLI's --only / --skip filters.
title string Human-readable description shown while the task runs.
config Config Your task's configuration object. Passed back to run as its first argument.
run function The work. Receives config and a TaskContext; returns a promise of the data to merge into globals.

Built-in tasks are factories that build a TaskDef for you from a config object — you rarely construct one by hand. For example, the whole of copyStaticTask is a real, tested TaskDef factory:

export interface CopyStaticConfig {
  from: string;
  to: string;
}
export function copyStaticTask(config: CopyStaticConfig): TaskDef<CopyStaticConfig> {
  return {
    name: 'copy-static',
    title: `Copy static files from ${config.from} to ${config.to}`,
    config,
    run: async (cfg, ctx) => {
      try {
        ctx.logger.debug(`Copying assets from ${cfg.from} to ${cfg.to}`);
        await ensureDir(cfg.to);
        await copyDir(cfg.from, cfg.to);
        ctx.logger.debug(`Copied assets from ${cfg.from} to ${cfg.to}`);
        return {};
      } catch (err) {
        throwTaskError(
          ctx,
          'copy-static',
          `Failed to copy assets from ${cfg.from} to ${cfg.to}`,
          err instanceof Error ? err : undefined,
        );
      }
    },
  };
}

The run() contract

run is where a task does its work. It's called once, with two arguments:

  • config — the config object the task was created with (typed as Config).
  • ctx — a TaskContext: the shared globals, a logger, and the debug flag.

Whatever run returns is shallow-merged into the shared globals. That is the only channel between tasks — a task reads earlier tasks' output from ctx.globals and contributes its own by returning an object:

const collectPostsTask = {
  name: 'collect-posts',
  config: {},
  run: async (config, ctx) => {
    ctx.logger.info('Collecting posts…');
    return { posts: ['Post 1', 'Post 2'] }; // merged into globals.posts
  },
};

const usePostsTask = {
  name: 'use-posts',
  config: {},
  run: async (config, ctx) => {
    ctx.logger.info(`Found ${ctx.globals.posts.length} posts`);
    // Returning nothing (undefined) contributes nothing to globals — fine.
  },
};

Return an object to add to globals; return nothing to contribute nothing. Read existing data from ctx.globals, don't mutate it — return new data instead. See Custom Tasks → The Context Object for the full authoring walkthrough.


TaskContext

Every run call receives the same shared context:

export interface TaskContext {
  logger: Logger;
  debug: boolean;
  globals: SkierGlobals;
}
Field Type Purpose
globals SkierGlobals Shared key-value data accumulated from every prior task's return value.
logger Logger Structured, level-based logging. Prefer this over console.log.
debug boolean true when the build was run with --debug. Gate verbose logging on it.

Logger

The logger is a small, level-based interface. debug output is only surfaced when the build runs with --debug:

export interface Logger {
  info(msg: string): void;
  warn(msg: string): void;
  error(msg: string): void;
  debug(msg: string): void;
}
run: async (config, ctx) => {
  ctx.logger.debug('Verbose detail, only shown with --debug');
  ctx.logger.info('Normal progress');
  ctx.logger.warn('Something looks off, continuing anyway');
  ctx.logger.error('Something failed');
};

Typing a custom task

In TypeScript, import the contract from the package root and let the generics carry your config and output shapes:

import type { TaskDef, TaskContext, SkierGlobals } from 'skier';

interface GreetConfig {
  name: string;
}

export const greetTask = (config: GreetConfig): TaskDef<GreetConfig, SkierGlobals> => ({
  name: 'greet',
  title: `Greet ${config.name}`,
  config,
  run: async (cfg, ctx: TaskContext) => {
    ctx.logger.info(`Hello, ${cfg.name}!`);
    return { greeted: cfg.name };
  },
});

In plain JavaScript you don't import anything — a task is just an object with name, config, and run.


  • Core TypesSkierItem, SkierGlobals, and the data built-in tasks produce.
  • Config Interfaces — the *Config type for each built-in.
  • Custom Tasks — the how-to guide, with patterns and testing.