Giter Club home page Giter Club logo

tasuku's Introduction


The minimal task runner for Node.js

Features

  • Task list with dynamic states
  • Parallel & nestable tasks
  • Unopinionated
  • Type-safe

Try it out online

Found this package useful? Show your support & appreciation by sponsoring! ❤️

Install

npm i tasuku

About

タスク (Tasuku) is a minimal task runner for Node.js. You can use it to label any task/function so that its loading, success, and error states are rendered in the terminal.

For example, here's a simple script that copies a file from path A to B.

import { copyFile } from 'fs/promises'
import task from 'tasuku'

task('Copying file from path A to B', async ({ setTitle }) => {
    await copyFile('/path/A', '/path/B')

    setTitle('Successfully copied file from path A to B!')
})

Running the script will look like this in the terminal:

Usage

Task list

Call task(taskTitle, taskFunction) to start a task and display it in a task list in the terminal.

import task from 'tasuku'

task('Task 1', async () => {
    await someAsyncTask()
})

task('Task 2', async () => {
    await someAsyncTask()
})

task('Task 3', async () => {
    await someAsyncTask()
})

Task states

  • ◽️ Pending The task is queued and has not started
  • 🔅 Loading The task is running
  • ⚠️ Warning The task completed with a warning
  • ❌ Error The task exited with an error
  • ✅ Success The task completed without error

Unopinionated

You can call task() from anywhere. There are no requirements. It is designed to be as unopinionated as possible not to interfere with your code.

The tasks will be displayed in the terminal in a consolidated list.

You can change the title of the task by calling setTitle().

import task from 'tasuku'

task('Task 1', async () => {
    await someAsyncTask()
})

// ...

someOtherCode()

// ...

task('Task 2', async ({ setTitle }) => {
    await someAsyncTask()

    setTitle('Task 2 complete')
})

Task return values

The return value of a task will be stored in the output .result property.

If using TypeScript, the type of .result will be inferred from the task function.

const myTask = await task('Task 2', async () => {
    await someAsyncTask()

    return 'Success'
})

console.log(myTask.result) // 'Success'

Nesting tasks

Tasks can be nested indefinitely. Nested tasks will be stacked hierarchically in the task list.

await task('Do task', async ({ task }) => {
    await someAsyncTask()

    await task('Do another task', async ({ task }) => {
        await someAsyncTask()

        await task('And another', async () => {
            await someAsyncTask()
        })
    })
})

Collapsing nested tasks

Call .clear() on the returned task API to collapse the nested task.

await task('Do task', async ({ task }) => {
    await someAsyncTask()

    const nestedTask = await task('Do another task', async ({ task }) => {
        await someAsyncTask()
    })

    nestedTask.clear()
})

Grouped tasks

Tasks can be grouped with task.group(). Pass in a function that returns an array of tasks to run them sequentially.

This is useful for displaying a queue of tasks that have yet to run.

const groupedTasks = await task.group(task => [
    task('Task 1', async () => {
        await someAsyncTask()

        return 'one'
    }),

    task('Waiting for Task 1', async ({ setTitle }) => {
        setTitle('Task 2 running...')

        await someAsyncTask()

        setTitle('Task 2 complete')

        return 'two'
    })

    // ...
])

console.log(groupedTasks) // [{ result: 'one' }, { result: 'two' }]

Running tasks in parallel

You can run tasks in parallel by passing in { concurrency: n } as the second argument in task.group().

const api = await task.group(task => [
    task(
        'Task 1',
        async () => await someAsyncTask()
    ),

    task(
        'Task 2',
        async () => await someAsyncTask()
    )

    // ...
], {
    concurrency: 2 // Number of tasks to run at a time
})

api.clear() // Clear output

Alternatively, you can also use the native Promise.all() if you prefer. The advantage of using task.group() is that you can limit concurrency, displays queued tasks as pending, and it returns an API to easily clear the results.

// No API
await Promise.all([
    task(
        'Task 1',
        async () => await someAsyncTask()
    ),

    task(
        'Task 2',
        async () => await someAsyncTask()
    )

    // ...
])

API

task(taskTitle, taskFunction)

Returns a Promise that resolves with object:

type TaskAPI = {
    // Result from taskFunction
    result: any

    // State of the task
    state: 'error' | 'warning' | 'success'

    // Invoke to clear the results from the terminal
    clear: () => void
}

taskTitle

Type: string

Required: true

The name of the task displayed.

taskFunction

Type:

type TaskFunction = (taskInnerApi: {
    task: createTask
    setTitle(title: string): void
    setStatus(status?: string): void
    setOutput(output: string | { message: string }): void
    setWarning(warning: Error | string): void
    setError(error: Error | string): void
}) => Promise<any>

Required: true

The task function. The return value will be stored in the .result property of the task() output object.

task

A task function to use for nesting.

setTitle()

Call with a string to change the task title.

setStatus()

Call with a string to set the status of the task. See image below.

setOutput()

Call with a string to set the output of the task. See image below.

setWarning()

Call with a string or Error instance to put the task in a warning state.

setError()

Call with a string or Error instance to put the task in an error state. Tasks automatically go into an error state when it catches an error in the task.

task.group(createTaskFunctions, options)

Returns a Promise that resolves with object:

// The results from the taskFunctions
type TaskGroupAPI = {
    // Result from taskFunction
    result: any

    // State of the task
    state: 'error' | 'warning' | 'success'

    // Invoke to clear the task result
    clear: () => void
}[] & {

    // Invoke to clear ALL results
    clear: () => void
}

createTaskFunctions

Type: (task) => Task[]

Required: true

A function that returns all the tasks you want to group in an array.

options

Directly passed into p-map.

concurrency

Type: number (Integer)

Default: 1

Number of tasks to run at a time.

stopOnError

Type: boolean

Default: true

When set to false, instead of stopping when a task fails, it will wait for all the tasks to finish and then reject with an aggregated error containing all the errors from the rejected promises.

FAQ

What does "Tasuku" mean?

Tasuku or タスク is the phonetic Japanese pronounciation of the word "task".

Why did you make this?

For writing scripts or CLI tools. Tasuku is a great way to convey the state of the tasks that are running in your script without being imposing about the way you write your code.

Major shoutout to listr + listr2 for being the motivation and visual inspiration for Tasuku, and for being my go-to task runner for a long time. I made Tasuku because I eventually found that they were too structured and declarative for my needs.

Big thanks to ink for doing all the heavy lifting for rendering interfaces in the terminal. Implementing a dynamic task list that doesn't interfere with console.logs() wouldn't have been so easy without it.

Doesn't the usage of nested task functions violate ESLint's no-shadow?

Yes, but it should be fine as you don't need access to other task functions aside from the immediate one.

Put task in the allow list:

  • "no-shadow": ["error", { "allow": ["task"] }]
  • "@typescript-eslint/no-shadow": ["error", { "allow": ["task"] }]

Sponsors

tasuku's People

Contributors

privatenumber avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tasuku's Issues

Group tasks from a generator

Is your feature request related to a problem?

I have a large dataset that I want to process as a grouped set of tasks. task.group forces me to create 1 function per task to run, it is slow and no efficient for memory.

Describe the solution you'd like

It would be more efficient to have something like task.map that takes a generator or an array of data, as well as a function to execute for each record.

example:

const data = function*() {
  yield 'a';
  yield 'b';
  yield 'c';
};

// or
// data = ['a', 'b', 'c'];

task.map(data, (task, item)=> task(item, () => 
  // so something
}));

Task.map can also manage concurrency and invoke the next function only when a seat is freed:

const data = function*() {
  yield 'a';
  yield 'b';
  yield 'c';
};
task.map(
  data, 
  (task, item)=> task(item, () => 
  // so something
  }),
  {concurrency: 2}
);

I'm happy to send a PR for this

Able to clear/unmount tasuku when top-level task throws error.

Is your feature request related to a problem?

When top-level task body throws, there are two issues:

Tasuku eats my stack trace
Tasuku does not allow me to intercept console (#16) when mounted.

Describe the solution you'd like

Not sure if I want to see the stack trace anywhere by default, but
I'd love to be able to clear/unmount tasuku completely in case error happens inside top-level task and fall back to own error handling.

Add esModuleInterop typescript option

Is your feature request related to a problem?

No

Describe the solution you'd like

When I install and use the tasuku library with typscript project
I have written code following this library document.

But I faced is not a function error and I founded issue point.

import task from "tasuku"

// it same below code without esModuleExport
const task = require("tasuku").default;

So I printed it and that is undefined.

Describe alternatives you've considered

Of course, it is not an important issue.

But How about the option was left in the readme.md?
I think it would be greater before then.

And I'm using this library very useful! thanks 😄

Additional context

Recommended way to use prompts?

Is your feature request related to a problem?

Not exactly - I currently have a CLI app that's using listr2, but tasuku looks nicer in that each task can have a return value, rather than listr2 which relies on mutating the context, which isn't as type safe.

listr2 has a specific prompt adapter. I'm not sure if tasuku would need an adapter, but it could be useful to have a docs suggestion of how you would recommend integrating enquirer or something

Describe the solution you'd like

What would be really great, is some sort of cleye x taskuku x enquirer thing. Where cleye defines its arguments/flags, andtasuku adds tasks to prompt the user for those arguments if they haven't been defined already. It could be built in userland, but wondered if there'd be interest in incorporating here.

Describe alternatives you've considered

DIY - just install enquirer.

Additional context

Access `warning`

If a completed task has a warning set, the parent should be able to check it to determine whether the task output can be cleared:

const result = task(...);

if (!result.warning) {
	result.clear();
}

Export types

I want to pass the task object to another function, like so.

import tsk, { TaskFunction } from 'tasuku'

async function scanPage(page: number, task: TaskFunction) {
    .......
    task.setOutput(`Scanning page ${page}...`)
    ........
}

tsk(
    'Scanning page',
    async (task) => {
        await scanPage(1, task)
    }
)

This doesn't work because TaskFunction is not exported.
Great library otherwise though, congrats, I switched from listr2

Unable to output message after task message

Bug description

I want to output a message AFTER all the tasks have run, but it seems Tasuku somehow always hijacks the order of messages. I tried to use .then() and await the task itself, but no success...

How can I ensure my message is displayed AFTER the task messages?

Reproduction

import task from 'tasuku';

const delay = (ms) => {
    return new Promise(function(resolve) {
        setTimeout(resolve, ms);
    });
}

const response = await
  task( `My task`, () => (delay(100)))
  .then(() => {
    console.log('THEN command');
  });

console.log('should print after response, no?');

Output

Screenshot 2022-04-08 at 11 59 06

Environment

  • tasuku version: 1.0.2
  • Operating System: OSX Monterey 12.2.1
  • Node version: v14.18.2
  • Package manager (npm/yarn/pnpm) and version:

Redraw glitch when list of task is long

Bug description

This is a visual bug. When the list of items is bigger than the screen height, the list starts glitching.

2021-11-30 09 55 01

Reproduction

const task = require("tasuku");

(async function test() {
  let iteration = 0;
  while (true) {
    await task(
      `Run ${iteration} -- with long text it is more visible`,
      () => new Promise((resolve) => setTimeout(() => resolve(), 100))
    );
    iteration++;
  }
})();

Environment

  • tasuku version: 1.0.2
  • Operating System: MacOS 12.0.1 with ITerm2 3.4.12
  • Node version: 14.18.0 and 15.14.0 (inside docker)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.