Foy Task Runner

repository·main·Indexed 43 days ago

https://github.com/zaaack/foy

A simple, lightweight, and modern general-purpose task runner featuring a promise-based API, built-in file system utilities, and support for task dependencies and namespaces. Version 1.1.2 includes a promise-based fs wrapper, a built-in logger, and shell command execution via ctx.$ (template literals) and ctx.exec (string syntax). It supports TypeScript and CoffeeScript via custom compilers and provides a ShellContext for managing directory stacks and environment variables.

Tokens
13.6K
Snippets
57
Records
62
Agent score
54%

What's inside Foy

  1. Organize tasks with namespaces

    main

    Use namespace to group tasks and prevent name collisions. Tasks inside a namespace are accessed via namespace:taskname. Namespaces support lifecycle hooks: before(), after(), and onerror().

    Nesting: Namespaces can be nested, resulting in colon-separated names like parent:child:task.

    import { task, namespace, logger } from 'foy'
    
    namespace('client', ns => {
      before(() => logger.info('before'))
      after(() => logger.info('after'))
      onerror(() => logger.info('onerror'))
    
      task('start', async ctx => { /* ... */ }) // accessed as client:start
    
      namespace('proj1', ns => {
        task('start', async ctx => { /* ... */ }) // accessed as client:proj1:start
      })
    })
    
    namespace('server', ns => {
      task('start', async ctx => { /* ... */ }) // accessed as server:start
    })
  2. Manage task dependencies

    main

    Tasks can depend on other tasks. By default, dependencies run serially. You can control their execution using the dep() helper or by passing an array of dependency objects.

    Dependency Options:

    • async: true: Run the dependency concurrently (parallelly) with other async dependencies.
    • force: true: Force the dependency to rerun even if it has been executed before.
    • priority: number: Higher values run earlier (used for async tasks).
    • options: object: Pass specific options to the dependency task.
    • resolveOptions: async (ctx) => object: A function to dynamically resolve options for the dependency.

    Example using the dep() sugar:

    import { task, dep } from 'foy'
    
    task(
      'publish:patch',
      [
        dep('test').async(0).force(),
        dep('build').async(1).force()
      ],
      async ctx => {
        await ctx.exec('npm version patch')
        await ctx.exec('npm publish')
      }
    )
  3. Install Foy

    main

    You can install Foy as a development dependency or globally using yarn or npm.

    As a development dependency:

    yarn add -D foy # or npm i -D foy

    Globally:

    yarn add -g foy # or npm i -g foy
    yarn add -D foy
  4. Initialize a Foyfile

    main

    A Foyfile defines your tasks. You can create a Foyfile.js or a Foyfile.ts (requires tsx, @swc-node/register, or ts-node for TypeScript support) in your project root.

    To generate a default Foyfile.js:

    foy --init

    To generate a Foyfile.ts:

    foy --init ts
    foy --init
  5. How task dependencies and async execution work

    main

    The TaskManager uses a dependency tree to manage task execution order. Tasks can define dependencies that are either synchronous or asynchronous.

    • Synchronous Dependencies: Tasks that must complete before the dependent task starts. These are processed sequentially.
    • Asynchronous Dependencies: Tasks that can run in parallel. You can specify a priority (via the async property) to group them. Tasks with the same priority level run concurrently using Promise.all, while different priority levels are executed in descending order of their priority value.
    • Dependency Resolution: The resolveDependencyTree method builds a DepsTree which tracks the TaskState (e.g., waiting, pending, loading, succeeded, failed, skipped) and the depth of the dependency.

    If a task has already been executed in the current session and force: true is not passed, the TaskManager will skip the task to optimize execution.

  6. Configure spinners for CI servers

    main

    By default, Foy uses CLI spinners for loading animations. In CI environments, these can clutter logs. You can disable them globally or for specific tasks.

    • setGlobalOptions({ spinner: true }): Enables loading animations (default is false).
    • spinner(false): Disables the spinner for the current task.
    import { task, spinner, setGlobalOptions } from 'foy'
    
    // Enable animations globally
    setGlobalOptions({ spinner: true })
    
    // Disable spinner for a specific task
    spinner(false)
    task('test', async ctx => { /* ... */ })
  7. How ShellContext manages directory state

    main

    The ShellContext maintains a directory stack to simulate shell navigation. This allows you to change the working directory for all subsequent commands executed through the context without affecting the actual process's process.cwd().

    • ctx.cwd: Returns the current working directory in the stack.
    • ctx.cd(dir): Changes the current directory (resolves relative to the current cwd).
    • ctx.pushd(dir): Pushes a new directory onto the stack.
    • ctx.popd(): Removes the top directory from the stack.
    await shell(async (ctx) => {
      ctx.pushd('./logs')
      await ctx.exec('touch app.log')
      ctx.popd()
      await ctx.exec('ls') // Back to original directory
    })
  8. Configure Foy execution options

    main

    While the CLI entrypoint logic is internal, it respects several configuration options that control how Foy spawns processes. These options are typically passed via the defaultCli configuration.

    Debugging and Inspection

    You can enable Node.js inspection by passing flags that set NODE_OPTIONS. This is useful for attaching debuggers to the processes Foy spawns.

    • --inspect <port|boolean>: Enables the inspector.
    • --inspectBrk <port|boolean>: Enables the inspector and breaks at the first line of code.

    Executor Override

    You can specify a custom executor to bypass the automatic detection logic. If an executor is provided, Foy will use it directly to run the foy files.

  9. Configure task dependencies

    main

    Tasks can depend on other tasks. Dependencies are executed serially by default. You can customize dependency behavior using the TaskDep configuration:

    • name: The name of the dependency task (string) or a TaskDep object.
    • async: If set to true, the task runs immediately (parallel to others). If set to a number, it defines the priority for asynchronized tasks (higher number = higher priority).
    • force: If true, the dependency will rerun even if it appears multiple times in the dependency tree.
    • resolveOptions: A function to resolve the dependency's options from the current context.
    import { task } from 'foy';
    
    task('setup', async () => { /* ... */ });
    
    task('build', [
      { name: 'setup', async: true }, // Run setup in parallel
      { name: 'test', force: true }    // Force rerun of test
    ], async (ctx) => {
      // ...
    });
  10. How Foy executes Foyfiles

    main

    Foy is a runner that executes one or more foy files (configuration or script files) found in your project. It automatically detects the appropriate runtime/executor based on the file extension and your project's environment.

    Execution Logic

    • JavaScript files (.js, .cjs, .mjs, .jsx): Executed using node.
    • TypeScript files (.ts, .cts, .tsx, .ctsx): Foy attempts to detect a TypeScript executor. It uses a priority-based detection system and caches the result in node_modules/.cache/foyCache.json to speed up subsequent runs.
    • Custom Executor: You can override the automatic detection by providing a specific executor via CLI options (see defaultCli.options.executor).

    Supported TypeScript Executors

    Foy searches for executors in the following order of priority:

    1. tsx (local node_modules/.bin/tsx)
    2. tsx (global)
    3. ts-node (local)
    4. @swc-node/register (if present in dependencies or devDependencies)
    5. swc-node (local)
    6. bun (global)
    7. npx (fallback)

    If no suitable executor is found, Foy will exit with an error.

  11. Organize tasks using namespaces

    main

    The namespace function allows you to group related tasks under a common prefix. This is useful for separating concerns like client and server tasks. When you define a task inside a namespace callback, the task's name is automatically prefixed with the namespace string, separated by a colon.

    Example usage:

    namespace('client', (ns) => {
      task('run', async (ctx) => {
        // This task is named 'client:run'
      });
    });

    To run these tasks from the CLI, use the prefixed name: foy client:run.

    import { namespace, task } from 'foy';
    
    namespace('client', ns => {
      task('run', async ctx => {
        // task name is 'client:run'
      })
    })
    
    namespace('server', ns => {
      task('run', async ctx => {
        // task name is 'server:run'
      })
    })
  12. Install Foy command completion

    main

    To enable shell autocompletion for Foy commands, generate the completion profile and append it to your shell configuration file (e.g., .bashrc or .zshrc).

    # For Bash
    foy --completion-profile >> ~/.bashrc
    
    # For Zsh
    foy --completion-profile >> ~/.zshrc