@tsed/cli-tasks 7.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/esm/domain/TaskLogger.js +270 -0
- package/lib/esm/fn/taskLogger.js +4 -0
- package/lib/esm/index.js +4 -0
- package/lib/esm/interfaces/Task.js +1 -0
- package/lib/esm/tasks.js +101 -0
- package/lib/tsconfig.esm.tsbuildinfo +1 -0
- package/lib/types/domain/TaskLogger.d.ts +37 -0
- package/lib/types/fn/taskLogger.d.ts +2 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/interfaces/Task.d.ts +12 -0
- package/lib/types/tasks.d.ts +12 -0
- package/package.json +53 -0
- package/src/domain/TaskLogger.spec.ts +324 -0
- package/src/domain/TaskLogger.ts +319 -0
- package/src/fn/taskLogger.ts +5 -0
- package/src/index.ts +4 -0
- package/src/interfaces/Task.ts +15 -0
- package/src/tasks.spec.ts +193 -0
- package/src/tasks.ts +131 -0
- package/test.js +68 -0
- package/tsconfig.esm.json +27 -0
- package/vitest.config.mts +20 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {DITest, runInContext} from "@tsed/di";
|
|
2
|
+
import {Observable} from "rxjs";
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest";
|
|
4
|
+
|
|
5
|
+
import {TaskLogger, type TaskLoggerOptions} from "./domain/TaskLogger.js";
|
|
6
|
+
import type {Task} from "./interfaces/Task.js";
|
|
7
|
+
import {concat, tasks} from "./tasks.js";
|
|
8
|
+
|
|
9
|
+
type LoggerStub = {
|
|
10
|
+
title: string;
|
|
11
|
+
type?: TaskLoggerOptions["type"];
|
|
12
|
+
parent?: TaskLogger;
|
|
13
|
+
max: number;
|
|
14
|
+
start: ReturnType<typeof vi.fn>;
|
|
15
|
+
done: ReturnType<typeof vi.fn>;
|
|
16
|
+
skip: ReturnType<typeof vi.fn>;
|
|
17
|
+
error: ReturnType<typeof vi.fn>;
|
|
18
|
+
message: ReturnType<typeof vi.fn>;
|
|
19
|
+
info: ReturnType<typeof vi.fn>;
|
|
20
|
+
warn: ReturnType<typeof vi.fn>;
|
|
21
|
+
advance: ReturnType<typeof vi.fn>;
|
|
22
|
+
log: ReturnType<typeof vi.fn>;
|
|
23
|
+
report: ReturnType<typeof vi.fn>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function createLoggerStub(opts: TaskLoggerOptions) {
|
|
27
|
+
const stub: LoggerStub = {
|
|
28
|
+
title: opts.title,
|
|
29
|
+
type: opts.type,
|
|
30
|
+
parent: opts.parent,
|
|
31
|
+
max: 0,
|
|
32
|
+
start: vi.fn().mockReturnThis(),
|
|
33
|
+
done: vi.fn().mockReturnThis(),
|
|
34
|
+
skip: vi.fn().mockReturnThis(),
|
|
35
|
+
error: vi.fn().mockReturnThis(),
|
|
36
|
+
message: vi.fn().mockReturnThis(),
|
|
37
|
+
info: vi.fn().mockReturnThis(),
|
|
38
|
+
warn: vi.fn().mockReturnThis(),
|
|
39
|
+
advance: vi.fn().mockReturnThis(),
|
|
40
|
+
log: vi.fn().mockReturnThis(),
|
|
41
|
+
report: vi.fn().mockReturnThis()
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return stub as LoggerStub & TaskLogger;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mockTaskLoggers() {
|
|
48
|
+
const loggers = new Map<string, LoggerStub & TaskLogger>();
|
|
49
|
+
vi.spyOn(TaskLogger, "from").mockImplementation((opts) => {
|
|
50
|
+
const stub = createLoggerStub(opts);
|
|
51
|
+
loggers.set(opts.title, stub);
|
|
52
|
+
return stub;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return loggers;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("tasks()", () => {
|
|
59
|
+
beforeEach(() =>
|
|
60
|
+
DITest.create({
|
|
61
|
+
env: "test"
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
vi.restoreAllMocks();
|
|
67
|
+
return DITest.reset();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("logs each emission from observable task results", async () => {
|
|
71
|
+
const loggers = mockTaskLoggers();
|
|
72
|
+
|
|
73
|
+
const observableTask$ = new Observable<string>((subscriber) => {
|
|
74
|
+
subscriber.next("first-line");
|
|
75
|
+
subscriber.next("second-line");
|
|
76
|
+
subscriber.complete();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const observableTask: Task = {
|
|
80
|
+
title: "Observable task",
|
|
81
|
+
task: () => observableTask$
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const ctx = DITest.createDIContext();
|
|
85
|
+
|
|
86
|
+
await runInContext(ctx, () => tasks([observableTask], {}));
|
|
87
|
+
|
|
88
|
+
const logger = loggers.get("Observable task");
|
|
89
|
+
expect(logger?.message).toHaveBeenCalledTimes(2);
|
|
90
|
+
expect(logger?.message).toHaveBeenNthCalledWith(1, "first-line");
|
|
91
|
+
expect(logger?.message).toHaveBeenNthCalledWith(2, "second-line");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("skips tasks when enabled predicate resolves false", async () => {
|
|
95
|
+
const loggers = mockTaskLoggers();
|
|
96
|
+
const disabledSpy = vi.fn();
|
|
97
|
+
const activeSpy = vi.fn();
|
|
98
|
+
|
|
99
|
+
const ctx = DITest.createDIContext();
|
|
100
|
+
await runInContext(ctx, () =>
|
|
101
|
+
tasks(
|
|
102
|
+
[
|
|
103
|
+
{
|
|
104
|
+
title: "Disabled task",
|
|
105
|
+
enabled: () => false,
|
|
106
|
+
task: disabledSpy
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
title: "Active task",
|
|
110
|
+
task: activeSpy
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
{}
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(disabledSpy).not.toHaveBeenCalled();
|
|
118
|
+
expect(activeSpy).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(loggers.get("Disabled task")?.skip).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(loggers.get("Active task")?.start).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(loggers.get("Active task")?.done).toHaveBeenCalledTimes(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("skips tasks when skip predicate resolves truthy", async () => {
|
|
125
|
+
const loggers = mockTaskLoggers();
|
|
126
|
+
const skippedSpy = vi.fn();
|
|
127
|
+
|
|
128
|
+
const ctx = DITest.createDIContext();
|
|
129
|
+
await runInContext(ctx, () =>
|
|
130
|
+
tasks(
|
|
131
|
+
[
|
|
132
|
+
{
|
|
133
|
+
title: "Skippable task",
|
|
134
|
+
skip: () => "skip",
|
|
135
|
+
task: skippedSpy
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
{}
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(skippedSpy).not.toHaveBeenCalled();
|
|
143
|
+
expect(loggers.get("Skippable task")?.skip).toHaveBeenCalledTimes(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("runs nested tasks returned from a task result", async () => {
|
|
147
|
+
const loggers = mockTaskLoggers();
|
|
148
|
+
const order: string[] = [];
|
|
149
|
+
|
|
150
|
+
const ctx = DITest.createDIContext();
|
|
151
|
+
await runInContext(ctx, () =>
|
|
152
|
+
tasks(
|
|
153
|
+
[
|
|
154
|
+
{
|
|
155
|
+
title: "Parent task",
|
|
156
|
+
task: () => [
|
|
157
|
+
{
|
|
158
|
+
title: "Child sync",
|
|
159
|
+
task: () => {
|
|
160
|
+
order.push("sync");
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
title: "Child async",
|
|
165
|
+
task: () =>
|
|
166
|
+
Promise.resolve().then(() => {
|
|
167
|
+
order.push("async");
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
{}
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(order).toEqual(["sync", "async"]);
|
|
178
|
+
expect(loggers.get("Child sync")?.start).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(loggers.get("Child async")?.done).toHaveBeenCalledTimes(1);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("concat()", () => {
|
|
184
|
+
it("merges task arrays while ignoring empty entries", async () => {
|
|
185
|
+
const taskA: Task = {title: "A", task: vi.fn()};
|
|
186
|
+
const taskB: Task = {title: "B", task: vi.fn()};
|
|
187
|
+
const taskC: Task = {title: "C", task: vi.fn()};
|
|
188
|
+
|
|
189
|
+
const result = await concat([taskA], undefined, [], [taskB, taskC]);
|
|
190
|
+
|
|
191
|
+
expect(result).toEqual([taskA, taskB, taskC]);
|
|
192
|
+
});
|
|
193
|
+
});
|
package/src/tasks.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {isArray} from "@tsed/core/utils/isArray";
|
|
2
|
+
import {isBoolean} from "@tsed/core/utils/isBoolean";
|
|
3
|
+
import {isFunction} from "@tsed/core/utils/isFunction";
|
|
4
|
+
import {isObservable} from "@tsed/core/utils/isObservable";
|
|
5
|
+
import {isPromise} from "@tsed/core/utils/isPromise";
|
|
6
|
+
import {isString} from "@tsed/core/utils/isString";
|
|
7
|
+
import {context} from "@tsed/di";
|
|
8
|
+
|
|
9
|
+
import {TaskLogger, type TaskLoggerOptions} from "./domain/TaskLogger.js";
|
|
10
|
+
import type {Task} from "./interfaces/Task.js";
|
|
11
|
+
|
|
12
|
+
export interface TasksOptions {
|
|
13
|
+
verbose?: TaskLoggerOptions["verbose"];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function tasks<T = any>(list: Task[], ctx: T & TasksOptions, parent?: TaskLogger) {
|
|
17
|
+
const items = list.filter((task) => isEnabled(task));
|
|
18
|
+
|
|
19
|
+
parent && (parent.max = items.length);
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < items.length; i++) {
|
|
22
|
+
const task = items[i];
|
|
23
|
+
|
|
24
|
+
const taskLogger = TaskLogger.from({
|
|
25
|
+
index: i,
|
|
26
|
+
title: task.title,
|
|
27
|
+
type: task.type,
|
|
28
|
+
parent,
|
|
29
|
+
verbose: ctx.verbose
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
if (!(await isExecutable(task, ctx))) {
|
|
34
|
+
taskLogger.skip();
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
taskLogger.start();
|
|
39
|
+
|
|
40
|
+
context().set("TASK_LOGGER", taskLogger);
|
|
41
|
+
|
|
42
|
+
const result = await resolveTaskResult(task.task(ctx, taskLogger), taskLogger);
|
|
43
|
+
|
|
44
|
+
context().delete("TASK_LOGGER");
|
|
45
|
+
|
|
46
|
+
if (isArray(result)) {
|
|
47
|
+
await tasks(result, ctx, taskLogger);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
taskLogger.done();
|
|
51
|
+
} catch (er) {
|
|
52
|
+
taskLogger.error(er);
|
|
53
|
+
throw er;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveTaskResult(result: any, logger: TaskLogger): Promise<any> | any {
|
|
59
|
+
if (!result) {
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isPromise(result)) {
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (isObservable(result)) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let subscription: any;
|
|
70
|
+
subscription = result.subscribe({
|
|
71
|
+
next(value: unknown) {
|
|
72
|
+
if (isString(value)) {
|
|
73
|
+
logger.message(value);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
complete() {
|
|
77
|
+
subscription?.unsubscribe?.();
|
|
78
|
+
resolve(undefined);
|
|
79
|
+
},
|
|
80
|
+
error(err: any) {
|
|
81
|
+
subscription?.unsubscribe?.();
|
|
82
|
+
reject(err);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function concat(...args: (Task[] | void | undefined)[]) {
|
|
92
|
+
const tasks: Task[] = [];
|
|
93
|
+
|
|
94
|
+
for (const arg of args) {
|
|
95
|
+
if (isArray(arg)) {
|
|
96
|
+
tasks.push(...arg);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return tasks;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isEnabled(item: Task<any>) {
|
|
104
|
+
return isBoolean(item.enabled) ? item.enabled : true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function isExecutable<CTX = any>(item: Task, ctx: CTX): Promise<boolean> {
|
|
108
|
+
if (isFunction(item.enabled)) {
|
|
109
|
+
const isEnable = await item.enabled(ctx);
|
|
110
|
+
|
|
111
|
+
if (!isEnable) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if ("skip" in item) {
|
|
117
|
+
const isSkipped = isFunction(item.skip) ? await item.skip(ctx) : item.skip;
|
|
118
|
+
|
|
119
|
+
if (isSkipped) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @deprecated use tasks function instead
|
|
129
|
+
*/
|
|
130
|
+
export const createTasksRunner = tasks;
|
|
131
|
+
export const createSubTasks = tasks;
|
package/test.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {log, progress, spinner, taskLog} from "@clack/prompts";
|
|
2
|
+
|
|
3
|
+
async function extracted() {
|
|
4
|
+
// const log1 = taskLog({title: "Starting task..."});
|
|
5
|
+
//
|
|
6
|
+
// await new Promise((r) => setTimeout(r, 1000));
|
|
7
|
+
//
|
|
8
|
+
// log1.message("Message");
|
|
9
|
+
//
|
|
10
|
+
// await new Promise((r) => setTimeout(r, 1000));
|
|
11
|
+
//
|
|
12
|
+
// const group = log1.group("Subtask 1");
|
|
13
|
+
// await new Promise((r) => setTimeout(r, 1000));
|
|
14
|
+
//
|
|
15
|
+
// group.message("Subtask message");
|
|
16
|
+
//
|
|
17
|
+
// await new Promise((r) => setTimeout(r, 1000));
|
|
18
|
+
//
|
|
19
|
+
// group.message("Subtask message");
|
|
20
|
+
//
|
|
21
|
+
// await new Promise((r) => setTimeout(r, 1000));
|
|
22
|
+
//
|
|
23
|
+
// group.success("Subtask message 2");
|
|
24
|
+
//
|
|
25
|
+
// await new Promise((r) => setTimeout(r, 1000));
|
|
26
|
+
//
|
|
27
|
+
// const group2 = log1.group("Subtask 2");
|
|
28
|
+
//
|
|
29
|
+
// await new Promise((r) => setTimeout(r, 1000));
|
|
30
|
+
//
|
|
31
|
+
// group2.message("Subtask 2 message");
|
|
32
|
+
//
|
|
33
|
+
// const spin = spinner(); //
|
|
34
|
+
// spin.start("Loading...");
|
|
35
|
+
//
|
|
36
|
+
// await new Promise((r) => setTimeout(r, 1000));
|
|
37
|
+
//
|
|
38
|
+
// spin.stop("Loading...");
|
|
39
|
+
// spin.clear();
|
|
40
|
+
//
|
|
41
|
+
// log1.success("Message");
|
|
42
|
+
//
|
|
43
|
+
|
|
44
|
+
// const task = taskLog({title: "Starting task..."});
|
|
45
|
+
const p = progress({
|
|
46
|
+
style: "block",
|
|
47
|
+
max: 100
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
p.start("Preparing...");
|
|
51
|
+
|
|
52
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
53
|
+
|
|
54
|
+
p.advance(10, "Install...");
|
|
55
|
+
|
|
56
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
57
|
+
|
|
58
|
+
p.advance(50, "Update...");
|
|
59
|
+
|
|
60
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
61
|
+
|
|
62
|
+
p.advance(90, "Update...");
|
|
63
|
+
//
|
|
64
|
+
// p.stop("Complete!");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await extracted();
|
|
68
|
+
// await extracted();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@tsed/typescript/tsconfig.node.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"outDir": "./lib/esm",
|
|
7
|
+
"declarationDir": "./lib/types",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"composite": true,
|
|
10
|
+
"noEmit": false,
|
|
11
|
+
"sourceMap": false
|
|
12
|
+
},
|
|
13
|
+
"include": ["src", "src/**/*.json"],
|
|
14
|
+
"exclude": [
|
|
15
|
+
"node_modules",
|
|
16
|
+
"test",
|
|
17
|
+
"lib",
|
|
18
|
+
"benchmark",
|
|
19
|
+
"coverage",
|
|
20
|
+
"spec",
|
|
21
|
+
"**/*.benchmark.ts",
|
|
22
|
+
"**/*.spec.ts",
|
|
23
|
+
"keys",
|
|
24
|
+
"**/__mock__/**",
|
|
25
|
+
"webpack.config.js"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import {presets} from "@tsed/vitest/presets";
|
|
3
|
+
import {defineConfig} from "vitest/config";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
...presets,
|
|
7
|
+
test: {
|
|
8
|
+
...presets.test,
|
|
9
|
+
setupFiles: ["reflect-metadata"],
|
|
10
|
+
coverage: {
|
|
11
|
+
...presets.test?.coverage,
|
|
12
|
+
thresholds: {
|
|
13
|
+
statements: 0,
|
|
14
|
+
branches: 0,
|
|
15
|
+
functions: 0,
|
|
16
|
+
lines: 0
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|