4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The minimal task runner for Node.js
5 |
6 |
7 | ### Features
8 | - Task list with dynamic states
9 | - Parallel & nestable tasks
10 | - Unopinionated
11 | - Type-safe
12 |
13 | → [Try it out online](https://stackblitz.com/edit/tasuku-demo?file=index.js&devtoolsheight=50&view=editor)
14 |
15 | Found this package useful? Show your support & appreciation by [sponsoring](https://github.com/sponsors/privatenumber)! ❤️
16 |
17 | ## Install
18 | ```sh
19 | npm i tasuku
20 | ```
21 |
22 | ## About
23 | タスク (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.
24 |
25 | For example, here's a simple script that copies a file from path A to B.
26 |
27 | ```ts
28 | import { copyFile } from 'fs/promises'
29 | import task from 'tasuku'
30 |
31 | task('Copying file from path A to B', async ({ setTitle }) => {
32 | await copyFile('/path/A', '/path/B')
33 |
34 | setTitle('Successfully copied file from path A to B!')
35 | })
36 | ```
37 |
38 | Running the script will look like this in the terminal:
39 |
40 |
41 |
42 | ## Usage
43 | ### Task list
44 | Call `task(taskTitle, taskFunction)` to start a task and display it in a task list in the terminal.
45 |
46 | ```ts
47 | import task from 'tasuku'
48 |
49 | task('Task 1', async () => {
50 | await someAsyncTask()
51 | })
52 |
53 | task('Task 2', async () => {
54 | await someAsyncTask()
55 | })
56 |
57 | task('Task 3', async () => {
58 | await someAsyncTask()
59 | })
60 | ```
61 |
62 |
63 |
64 | #### Task states
65 | - **◽️ Pending** The task is queued and has not started
66 | - **🔅 Loading** The task is running
67 | - **⚠️ Warning** The task completed with a warning
68 | - **❌ Error** The task exited with an error
69 | - **✅ Success** The task completed without error
70 |
71 |
72 |
73 | ### Unopinionated
74 | 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.
75 |
76 | The tasks will be displayed in the terminal in a consolidated list.
77 |
78 | You can change the title of the task by calling `setTitle()`.
79 | ```ts
80 | import task from 'tasuku'
81 |
82 | task('Task 1', async () => {
83 | await someAsyncTask()
84 | })
85 |
86 | // ...
87 |
88 | someOtherCode()
89 |
90 | // ...
91 |
92 | task('Task 2', async ({ setTitle }) => {
93 | await someAsyncTask()
94 |
95 | setTitle('Task 2 complete')
96 | })
97 | ```
98 |
99 |
100 |
101 | ### Task return values
102 | The return value of a task will be stored in the output `.result` property.
103 |
104 | If using TypeScript, the type of `.result` will be inferred from the task function.
105 |
106 | ```ts
107 | const myTask = await task('Task 2', async () => {
108 | await someAsyncTask()
109 |
110 | return 'Success'
111 | })
112 |
113 | console.log(myTask.result) // 'Success'
114 | ```
115 |
116 | ### Nesting tasks
117 | Tasks can be nested indefinitely. Nested tasks will be stacked hierarchically in the task list.
118 | ```ts
119 | await task('Do task', async ({ task }) => {
120 | await someAsyncTask()
121 |
122 | await task('Do another task', async ({ task }) => {
123 | await someAsyncTask()
124 |
125 | await task('And another', async () => {
126 | await someAsyncTask()
127 | })
128 | })
129 | })
130 | ```
131 |
132 |
133 |
134 | ### Collapsing nested tasks
135 | Call `.clear()` on the returned task API to collapse the nested task.
136 | ```ts
137 | await task('Do task', async ({ task }) => {
138 | await someAsyncTask()
139 |
140 | const nestedTask = await task('Do another task', async ({ task }) => {
141 | await someAsyncTask()
142 | })
143 |
144 | nestedTask.clear()
145 | })
146 | ```
147 |
148 |
149 |
150 | ### Grouped tasks
151 | Tasks can be grouped with `task.group()`. Pass in a function that returns an array of tasks to run them sequentially.
152 |
153 | This is useful for displaying a queue of tasks that have yet to run.
154 |
155 | ```ts
156 | const groupedTasks = await task.group(task => [
157 | task('Task 1', async () => {
158 | await someAsyncTask()
159 |
160 | return 'one'
161 | }),
162 |
163 | task('Waiting for Task 1', async ({ setTitle }) => {
164 | setTitle('Task 2 running...')
165 |
166 | await someAsyncTask()
167 |
168 | setTitle('Task 2 complete')
169 |
170 | return 'two'
171 | })
172 |
173 | // ...
174 | ])
175 |
176 | console.log(groupedTasks) // [{ result: 'one' }, { result: 'two' }]
177 | ```
178 |
179 |
180 |
181 | ### Running tasks in parallel
182 | You can run tasks in parallel by passing in `{ concurrency: n }` as the second argument in `task.group()`.
183 |
184 | ```ts
185 | const api = await task.group(task => [
186 | task(
187 | 'Task 1',
188 | async () => await someAsyncTask()
189 | ),
190 |
191 | task(
192 | 'Task 2',
193 | async () => await someAsyncTask()
194 | )
195 |
196 | // ...
197 | ], {
198 | concurrency: 2 // Number of tasks to run at a time
199 | })
200 |
201 | api.clear() // Clear output
202 | ```
203 |
204 |
205 |
206 | 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.
207 |
208 | ```ts
209 | // No API
210 | await Promise.all([
211 | task(
212 | 'Task 1',
213 | async () => await someAsyncTask()
214 | ),
215 |
216 | task(
217 | 'Task 2',
218 | async () => await someAsyncTask()
219 | )
220 |
221 | // ...
222 | ])
223 | ```
224 |
225 | ## API
226 |
227 | ### task(taskTitle, taskFunction)
228 |
229 | Returns a Promise that resolves with object:
230 | ```ts
231 | type TaskAPI = {
232 | // Result from taskFunction
233 | result: unknown
234 |
235 | // State of the task
236 | state: 'error' | 'warning' | 'success'
237 |
238 | // Invoke to clear the results from the terminal
239 | clear: () => void
240 | }
241 | ```
242 |
243 | #### taskTitle
244 | Type: `string`
245 |
246 | Required: true
247 |
248 | The name of the task displayed.
249 |
250 | #### taskFunction
251 | Type:
252 | ```ts
253 | type TaskFunction = (taskInnerApi: {
254 | task: createTask
255 | setTitle(title: string): void
256 | setStatus(status?: string): void
257 | setOutput(output: string | { message: string }): void
258 | setWarning(warning: Error | string): void
259 | setError(error: Error | string): void
260 | }) => Promise
261 | ```
262 |
263 | Required: true
264 |
265 | The task function. The return value will be stored in the `.result` property of the `task()` output object.
266 |
267 |
268 | #### task
269 | A task function to use for nesting.
270 |
271 | #### setTitle()
272 | Call with a string to change the task title.
273 |
274 | #### setStatus()
275 | Call with a string to set the status of the task. See image below.
276 |
277 | #### setOutput()
278 | Call with a string to set the output of the task. See image below.
279 |
280 | #### setWarning()
281 | Call with a string or Error instance to put the task in a warning state.
282 |
283 | #### setError()
284 | 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.
285 |
286 |
287 |
288 |
289 | ### task.group(createTaskFunctions, options)
290 | Returns a Promise that resolves with object:
291 | ```ts
292 | // The results from the taskFunctions
293 | type TaskGroupAPI = {
294 | // Result from taskFunction
295 | result: unknown
296 |
297 | // State of the task
298 | state: 'error' | 'warning' | 'success'
299 |
300 | // Invoke to clear the task result
301 | clear: () => void
302 | }[] & {
303 |
304 | // Invoke to clear ALL results
305 | clear: () => void
306 | }
307 | ```
308 |
309 | #### createTaskFunctions
310 | Type: `(task) => Task[]`
311 |
312 | Required: true
313 |
314 | A function that returns all the tasks you want to group in an array.
315 |
316 | #### options
317 |
318 | Directly passed into [`p-map`](https://github.com/sindresorhus/p-map).
319 |
320 | ##### concurrency
321 | Type: `number` (Integer)
322 |
323 | Default: `1`
324 |
325 | Number of tasks to run at a time.
326 |
327 | ##### stopOnError
328 | Type: `boolean`
329 |
330 | Default: `true`
331 |
332 | 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.
333 |
334 | ## FAQ
335 |
336 | ### What does "Tasuku" mean?
337 | _Tasuku_ or タスク is the phonetic Japanese pronounciation of the word "task".
338 |
339 |
340 | ### Why did you make this?
341 |
342 | 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.
343 |
344 | Major shoutout to [listr](https://github.com/SamVerschueren/listr) + [listr2](https://github.com/cenk1cenk2/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.
345 |
346 | Big thanks to [ink](https://github.com/vadimdemedes/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.
347 |
348 | ### Doesn't the usage of nested `task` functions violate ESLint's [no-shadow](https://eslint.org/docs/rules/no-shadow)?
349 | Yes, but it should be fine as you don't need access to other `task` functions aside from the immediate one.
350 |
351 | Put `task` in the allow list:
352 | - `"no-shadow": ["error", { "allow": ["task"] }]`
353 | - `"@typescript-eslint/no-shadow": ["error", { "allow": ["task"] }]`
354 |
355 |
356 | ## Sponsors
357 |
358 |
359 |
360 |
361 |
362 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tasuku",
3 | "version": "0.0.0-semantic-release",
4 | "description": "タスク — The minimal task runner",
5 | "keywords": [
6 | "simple",
7 | "minimal",
8 | "task",
9 | "runner",
10 | "cli"
11 | ],
12 | "license": "MIT",
13 | "repository": "privatenumber/tasuku",
14 | "funding": "https://github.com/privatenumber/tasuku?sponsor=1",
15 | "author": {
16 | "name": "Hiroki Osame",
17 | "email": "hiroki.osame@gmail.com"
18 | },
19 | "files": [
20 | "dist"
21 | ],
22 | "type": "module",
23 | "main": "./dist/index.cjs",
24 | "module": "./dist/index.mjs",
25 | "types": "./dist/index.d.cts",
26 | "exports": {
27 | "require": {
28 | "types": "./dist/index.d.cts",
29 | "default": "./dist/index.cjs"
30 | },
31 | "import": {
32 | "types": "./dist/index.d.mts",
33 | "default": "./dist/index.mjs"
34 | }
35 | },
36 | "imports": {
37 | "#tasuku": {
38 | "types": "./src/index.ts",
39 | "development": "./src/index.ts",
40 | "default": "./dist/index.mjs"
41 | }
42 | },
43 | "packageManager": "pnpm@9.15.4",
44 | "scripts": {
45 | "lint": "lintroll --cache .",
46 | "test": "tsx tests/tasuku.spec.ts",
47 | "test:tsd": "tsd",
48 | "type-check": "tsc",
49 | "build": "pkgroll --env.NODE_ENV=production --env.DEV=false --minify",
50 | "prepack": "clean-pkg-json"
51 | },
52 | "dependencies": {
53 | "yoga-layout-prebuilt": "1.10.0"
54 | },
55 | "devDependencies": {
56 | "@types/node": "^18.11.18",
57 | "@types/react": "^18.0.27",
58 | "clean-pkg-json": "^1.2.0",
59 | "ink": "github:privatenumber/ink#built/treeshake-lodash",
60 | "ink-task-list": "^2.0.0",
61 | "lintroll": "^1.15.0",
62 | "manten": "^1.3.0",
63 | "p-map": "^5.3.0",
64 | "pkgroll": "^2.8.2",
65 | "react": "^17.0.2",
66 | "tsd": "^0.31.2",
67 | "tsx": "^4.19.2",
68 | "typescript": "^5.7.3",
69 | "valtio": "^1.2.11"
70 | },
71 | "tsd": {
72 | "directory": "tests"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/CreateApp.tsx:
--------------------------------------------------------------------------------
1 | import { render } from 'ink';
2 | import React from 'react';
3 | import type { TaskList } from '../types.js';
4 | import TaskListApp from './TaskListApp.js';
5 |
6 | export const createApp = (taskList: TaskList) => {
7 | const inkApp = render();
8 |
9 | return {
10 | remove: () => {
11 | inkApp.rerender(null);
12 | inkApp.unmount();
13 | inkApp.clear();
14 | inkApp.cleanup();
15 | },
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/TaskListApp.tsx:
--------------------------------------------------------------------------------
1 | import React, { type FC } from 'react';
2 | import { TaskList } from 'ink-task-list';
3 | import { useSnapshot } from 'valtio';
4 | import type { TaskObject } from '../types.js';
5 | import TaskListItem from './TaskListItem.js';
6 |
7 | const TaskListApp: FC<{
8 | taskList: TaskObject[];
9 | }> = ({
10 | taskList,
11 | }) => {
12 | const state = useSnapshot(taskList);
13 |
14 | return (
15 |
16 | {
17 | state.map((task, index) => (
18 |
22 | ))
23 | }
24 |
25 | );
26 | };
27 |
28 | export default TaskListApp;
29 |
--------------------------------------------------------------------------------
/src/components/TaskListItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { type FC } from 'react';
2 | import { Task } from 'ink-task-list';
3 | import type { TaskObject } from '../types.js';
4 |
5 | type DeepReadonly = { readonly [K in keyof T]: DeepReadonly };
6 |
7 | // From: https://github.com/sindresorhus/cli-spinners/blob/00de8fbeee16fa49502fa4f687449f70f2c8ca2c/spinners.json#L2
8 | const spinner = {
9 | interval: 80,
10 | frames: [
11 | '⠋',
12 | '⠙',
13 | '⠹',
14 | '⠸',
15 | '⠼',
16 | '⠴',
17 | '⠦',
18 | '⠧',
19 | '⠇',
20 | '⠏',
21 | ],
22 | };
23 |
24 | const TaskListItem: FC<{
25 | task: DeepReadonly;
26 | }> = ({
27 | task,
28 | }) => {
29 | const childTasks = (
30 | task.children.length > 0
31 | ? task.children.map((childTask, index) => (
32 |
36 | ))
37 | : []
38 | );
39 |
40 | return (
41 | 0}
48 | >
49 | {childTasks}
50 |
51 | );
52 | };
53 |
54 | export default TaskListItem;
55 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { proxy } from 'valtio';
2 | import pMap from 'p-map';
3 | import { arrayAdd, arrayRemove } from './utils.js';
4 | import { createApp } from './components/CreateApp.jsx';
5 | import {
6 | type TaskList,
7 | type TaskObject,
8 | type Task,
9 | type TaskAPI,
10 | type TaskInnerAPI,
11 | type TaskGroupAPI,
12 | type TaskFunction,
13 | type RegisteredTask, runSymbol,
14 | } from './types.js';
15 |
16 | const createTaskInnerApi = (taskState: TaskObject) => {
17 | const api: TaskInnerAPI = {
18 | task: createTaskFunction(taskState.children),
19 | setTitle(title) {
20 | taskState.title = title;
21 | },
22 | setStatus(status) {
23 | taskState.status = status;
24 | },
25 | setOutput(output) {
26 | taskState.output = (
27 | typeof output === 'string'
28 | ? output
29 | : (
30 | 'message' in output
31 | ? output.message
32 | : ''
33 | )
34 | );
35 | },
36 | setWarning(warning) {
37 | taskState.state = 'warning';
38 |
39 | if (warning !== undefined) {
40 | api.setOutput(warning);
41 | }
42 | },
43 | setError(error) {
44 | taskState.state = 'error';
45 |
46 | if (error !== undefined) {
47 | api.setOutput(error);
48 | }
49 | },
50 | };
51 | return api;
52 | };
53 |
54 | let app: ReturnType | undefined;
55 |
56 | const registerTask = (
57 | taskList: TaskList,
58 | taskTitle: string,
59 | taskFunction: TaskFunction,
60 | ): RegisteredTask => {
61 | if (!app) {
62 | app = createApp(taskList);
63 | taskList.isRoot = true;
64 | }
65 |
66 | const task = arrayAdd(taskList, {
67 | title: taskTitle,
68 | state: 'pending',
69 | children: [],
70 | });
71 |
72 | return {
73 | task,
74 | [runSymbol]: async () => {
75 | const api = createTaskInnerApi(task);
76 |
77 | task.state = 'loading';
78 |
79 | let taskResult;
80 | try {
81 | taskResult = await taskFunction(api);
82 | } catch (error) {
83 | api.setError(error as Error);
84 | throw error;
85 | }
86 |
87 | if (task.state === 'loading') {
88 | task.state = 'success';
89 | }
90 |
91 | return taskResult;
92 | },
93 | clear: () => {
94 | arrayRemove(taskList, task);
95 |
96 | if (taskList.isRoot && taskList.length === 0) {
97 | app!.remove();
98 | app = undefined;
99 | }
100 | },
101 | };
102 | };
103 |
104 | function createTaskFunction(
105 | taskList: TaskList,
106 | ): Task {
107 | const task: Task = async (
108 | title,
109 | taskFunction,
110 | ) => {
111 | const registeredTask = registerTask(taskList, title, taskFunction);
112 | const result = await registeredTask[runSymbol]();
113 |
114 | return {
115 | result,
116 | get state() {
117 | return registeredTask.task.state;
118 | },
119 | clear: registeredTask.clear,
120 | };
121 | };
122 |
123 | task.group = async (
124 | createTasks,
125 | options,
126 | ) => {
127 | const tasksQueue = createTasks((
128 | title,
129 | taskFunction,
130 | ) => registerTask(
131 | taskList,
132 | title,
133 | taskFunction,
134 | ));
135 |
136 | const results = (await pMap(
137 | tasksQueue,
138 | async taskApi => ({
139 | result: await taskApi[runSymbol](),
140 | get state() {
141 | return taskApi.task.state;
142 | },
143 | clear: taskApi.clear,
144 | }),
145 | {
146 | concurrency: 1,
147 | ...options,
148 | },
149 | )) as any; // eslint-disable-line @typescript-eslint/no-explicit-any -- Temporary fix
150 |
151 | return Object.assign(results, {
152 | clear: () => {
153 | for (const taskApi of tasksQueue) {
154 | taskApi.clear();
155 | }
156 | },
157 | });
158 | };
159 |
160 | return task;
161 | }
162 |
163 | const rootTaskList = proxy([]);
164 |
165 | export default createTaskFunction(rootTaskList);
166 | export type {
167 | Task,
168 | TaskAPI,
169 | TaskInnerAPI,
170 | TaskFunction,
171 | TaskGroupAPI,
172 | };
173 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { Options } from 'p-map';
2 |
3 | type State = 'pending' | 'loading' | 'error' | 'warning' | 'success';
4 |
5 | export type TaskObject = {
6 | title: string;
7 | state: State;
8 | children: TaskObject[];
9 | status?: string;
10 | output?: string;
11 | };
12 |
13 | export type TaskList = TaskObject[] & {
14 | isRoot?: boolean;
15 | };
16 |
17 | export type TaskInnerAPI = {
18 | task: Task;
19 | setTitle(title: string): void;
20 | setStatus(status?: string): void;
21 | setWarning(warning?: Error | string): void;
22 | setError(error?: Error | string): void;
23 | setOutput(output: string | { message: string }): void;
24 | };
25 |
26 | export type TaskFunction = (innerApi: TaskInnerAPI) => Promise;
27 |
28 | export const runSymbol: unique symbol = Symbol('run');
29 |
30 | export type RegisteredTask = {
31 | [runSymbol]: () => Promise; // ReturnType>;
32 | task: TaskObject;
33 | clear: () => void;
34 | };
35 |
36 | export type TaskAPI = {
37 | result: Result;
38 | state: State;
39 | clear: () => void;
40 | };
41 |
42 | export type Task = (
43 | (
44 |
45 | /**
46 | * The task title
47 | */
48 | title: string,
49 |
50 | /**
51 | * The task function
52 | */
53 | taskFunction: TaskFunction
54 | ) => Promise>
55 | ) & { group: TaskGroup };
56 |
57 | type TaskGroupResults<
58 | RegisteredTasks extends RegisteredTask[]
59 | > = {
60 | [Key in keyof RegisteredTasks]: (
61 | RegisteredTasks[Key] extends RegisteredTask
62 | ? TaskAPI
63 | : unknown
64 | );
65 | };
66 |
67 | export type TaskGroupAPI = Results & {
68 | clear(): void;
69 | };
70 |
71 | export type CreateTask = (
72 |
73 | /**
74 | * The task title
75 | */
76 | title: string,
77 |
78 | /**
79 | * The task function
80 | */
81 | taskFunction: TaskFunction,
82 | ) => RegisteredTask;
83 |
84 | type TaskGroup = <
85 | RegisteredTasks extends RegisteredTask[]
86 | >(
87 | createTasks: (taskCreator: CreateTask) => readonly [...RegisteredTasks],
88 | options?: Options
89 | ) => Promise>>;
90 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * These utilities are useful because Valtio
3 | * makes the elements reactive after adding to
4 | * the array.
5 | */
6 |
7 | export const arrayAdd = (
8 | array: T[],
9 | element: T,
10 | ) => {
11 | const index = array.push(element) - 1;
12 | return array[index];
13 | };
14 |
15 | export const arrayRemove = (
16 | array: T[],
17 | element: T,
18 | ) => {
19 | const index = array.indexOf(element);
20 | if (index !== -1) {
21 | array.splice(index, 1);
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/tests/tasuku.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, describe, expect } from 'manten';
2 | import task from '#tasuku';
3 |
4 | const sleep = (ms: number): Promise => new Promise((resolve) => {
5 | setTimeout(resolve, ms);
6 | });
7 |
8 | test('task - return number', async () => {
9 | const { result } = await task('Some task', async () => 1 + 1);
10 | expect(result).toBe(2);
11 | });
12 |
13 | test('task - return string', async () => {
14 | const { result } = await task('Some task', async () => 'some string');
15 | expect(result).toBe('some string');
16 | });
17 |
18 | test('task return Promise', async () => {
19 | const { result } = await task(
20 | 'Some task',
21 | async () => await new Promise((resolve) => {
22 | resolve(123);
23 | }),
24 | );
25 | expect(result).toBe(123);
26 | });
27 |
28 | test('nested tasks', async () => {
29 | const someTask = await task('Some task', async ({ task }) => {
30 | const nestedTask = await task('nested task', async () => 'nested works');
31 | expect(nestedTask.result).toBe('nested works');
32 |
33 | return 1;
34 | });
35 |
36 | expect(someTask.result).toBe(1);
37 | });
38 |
39 | describe('group tasks', ({ test }) => {
40 | test('task results', async () => {
41 | const groupTasks = await task.group(task => [
42 | task('number', async () => 123),
43 | task('string', async () => 'hello'),
44 | task('boolean', async () => false),
45 | ]);
46 |
47 | expect<{ result: number }>(groupTasks[0]).toMatchObject({
48 | state: 'success',
49 | result: 123,
50 | });
51 | expect<{ result: string }>(groupTasks[1]).toMatchObject({
52 | state: 'success',
53 | result: 'hello',
54 | });
55 | expect<{ result: boolean }>(groupTasks[2]).toMatchObject({
56 | state: 'success',
57 | result: false,
58 | });
59 | });
60 |
61 | test('concurrency - series', async () => {
62 | const startTime = Date.now();
63 | const groupTasks = await task.group(task => [
64 | task('one', async () => {
65 | await sleep(100);
66 | return 1;
67 | }),
68 | task('two', async () => {
69 | await sleep(100);
70 | return 2;
71 | }),
72 | task('three', async () => {
73 | await sleep(100);
74 | return 3;
75 | }),
76 | ]);
77 |
78 | const elapsed = Date.now() - startTime;
79 |
80 | expect(elapsed > 300 && elapsed < 400).toBe(true);
81 |
82 | expect<{ result: number }>(groupTasks[0]).toMatchObject({
83 | state: 'success',
84 | result: 1,
85 | });
86 | expect<{ result: number }>(groupTasks[1]).toMatchObject({
87 | state: 'success',
88 | result: 2,
89 | });
90 | expect<{ result: number }>(groupTasks[2]).toMatchObject({
91 | state: 'success',
92 | result: 3,
93 | });
94 | });
95 |
96 | test('concurrency - parallel', async () => {
97 | const startTime = Date.now();
98 | const groupTasks = await task.group(task => [
99 | task('one', async () => {
100 | await sleep(100);
101 | return 1;
102 | }),
103 | task('two', async () => {
104 | await sleep(100);
105 | return 2;
106 | }),
107 | task('three', async () => {
108 | await sleep(100);
109 | return 3;
110 | }),
111 | ], { concurrency: Number.POSITIVE_INFINITY });
112 |
113 | const elapsed = Date.now() - startTime;
114 |
115 | expect(elapsed > 100 && elapsed < 300).toBe(true);
116 |
117 | expect<{ result: number }>(groupTasks[0]).toMatchObject({
118 | state: 'success',
119 | result: 1,
120 | });
121 | expect<{ result: number }>(groupTasks[1]).toMatchObject({
122 | state: 'success',
123 | result: 2,
124 | });
125 | expect<{ result: number }>(groupTasks[2]).toMatchObject({
126 | state: 'success',
127 | result: 3,
128 | });
129 | });
130 | });
131 |
--------------------------------------------------------------------------------
/tests/tasuku.test-d.ts:
--------------------------------------------------------------------------------
1 | import { expectType } from 'tsd';
2 | import task from '#tasuku';
3 |
4 | const booleanResult = await task('title', async () => false);
5 | expectType(booleanResult.result);
6 |
7 | const numberResult = await task('title', async () => 123);
8 | expectType(numberResult.result);
9 |
10 | const stringResult = await task('title', async () => 'string');
11 | expectType(stringResult.result);
12 |
13 | const groupApi = await task.group(task => [
14 | task('title', async () => false),
15 | task('title', async () => 123),
16 | task('title', async () => 'string'),
17 | ]);
18 |
19 | expectType(groupApi[0].result);
20 | expectType(groupApi[1].result);
21 | expectType(groupApi[2].result);
22 |
--------------------------------------------------------------------------------
/tests/test.js:
--------------------------------------------------------------------------------
1 | import task from '../dist/index.mjs';
2 |
3 | task(`hi ${process.version}`, () => {});
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 |
4 | // Node 12
5 | "target": "ES2019",
6 | "jsx": "react",
7 | "module": "Preserve",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "isolatedModules": true,
11 | "skipLibCheck": true,
12 | "noEmit": true,
13 | },
14 | }
15 |
--------------------------------------------------------------------------------