@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.
@@ -0,0 +1,324 @@
1
+ import {contextLogger, DITest, runInContext} from "@tsed/di";
2
+ import type {MockInstance} from "vitest";
3
+ import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
4
+
5
+ import type {TaskLogger as TaskLoggerClass} from "./TaskLogger.js";
6
+
7
+ type ProgressStub = {
8
+ start: ReturnType<typeof vi.fn>;
9
+ stop: ReturnType<typeof vi.fn>;
10
+ advance: ReturnType<typeof vi.fn>;
11
+ };
12
+
13
+ type SpinnerStub = {
14
+ start: ReturnType<typeof vi.fn>;
15
+ stop: ReturnType<typeof vi.fn>;
16
+ message: ReturnType<typeof vi.fn>;
17
+ };
18
+
19
+ type TaskLogStub = {
20
+ title: string;
21
+ info: ReturnType<typeof vi.fn>;
22
+ warn: ReturnType<typeof vi.fn>;
23
+ error: ReturnType<typeof vi.fn>;
24
+ message: ReturnType<typeof vi.fn>;
25
+ success: ReturnType<typeof vi.fn>;
26
+ group: ReturnType<typeof vi.fn>;
27
+ };
28
+
29
+ type LogMock = {
30
+ info: ReturnType<typeof vi.fn>;
31
+ warn: ReturnType<typeof vi.fn>;
32
+ error: ReturnType<typeof vi.fn>;
33
+ message: ReturnType<typeof vi.fn>;
34
+ success: ReturnType<typeof vi.fn>;
35
+ };
36
+
37
+ type ClackMocks = {
38
+ logMock: LogMock;
39
+ progressInstances: ProgressStub[];
40
+ spinnerInstances: SpinnerStub[];
41
+ taskLogInstances: TaskLogStub[];
42
+ progressMock: ReturnType<typeof vi.fn>;
43
+ spinnerMock: ReturnType<typeof vi.fn>;
44
+ taskLogMock: ReturnType<typeof vi.fn>;
45
+ };
46
+
47
+ const clack = vi.hoisted(() => {
48
+ const logMock: LogMock = {
49
+ info: vi.fn(),
50
+ warn: vi.fn(),
51
+ error: vi.fn(),
52
+ message: vi.fn(),
53
+ success: vi.fn()
54
+ };
55
+
56
+ const progressInstances: ProgressStub[] = [];
57
+ const spinnerInstances: SpinnerStub[] = [];
58
+ const taskLogInstances: TaskLogStub[] = [];
59
+
60
+ const createProgressInstance = () => {
61
+ const instance: ProgressStub = {
62
+ start: vi.fn(),
63
+ stop: vi.fn(),
64
+ advance: vi.fn()
65
+ };
66
+
67
+ progressInstances.push(instance);
68
+ return instance;
69
+ };
70
+
71
+ const createSpinnerInstance = () => {
72
+ const instance: SpinnerStub = {
73
+ start: vi.fn(),
74
+ stop: vi.fn(),
75
+ message: vi.fn()
76
+ };
77
+
78
+ spinnerInstances.push(instance);
79
+ return instance;
80
+ };
81
+
82
+ const createTaskLogInstance = (title: string): TaskLogStub => {
83
+ const instance: TaskLogStub = {
84
+ title,
85
+ info: vi.fn(),
86
+ warn: vi.fn(),
87
+ error: vi.fn(),
88
+ message: vi.fn(),
89
+ success: vi.fn(),
90
+ group: vi.fn()
91
+ };
92
+
93
+ instance.group.mockImplementation((childTitle: string) => createTaskLogInstance(childTitle));
94
+ taskLogInstances.push(instance);
95
+
96
+ return instance;
97
+ };
98
+
99
+ return {
100
+ logMock,
101
+ progressInstances,
102
+ spinnerInstances,
103
+ taskLogInstances,
104
+ progressMock: vi.fn(() => createProgressInstance()),
105
+ spinnerMock: vi.fn(() => createSpinnerInstance()),
106
+ taskLogMock: vi.fn(({title}: {title: string}) => createTaskLogInstance(title))
107
+ };
108
+ }) as ClackMocks;
109
+
110
+ vi.mock("@clack/prompts", () => ({
111
+ log: clack.logMock,
112
+ progress: clack.progressMock,
113
+ spinner: clack.spinnerMock,
114
+ taskLog: clack.taskLogMock
115
+ }));
116
+
117
+ const {logMock, progressInstances, spinnerInstances, taskLogInstances} = clack;
118
+ const originalNodeEnv = process.env.NODE_ENV;
119
+
120
+ describe("TaskLogger", () => {
121
+ beforeEach(() => {
122
+ vi.clearAllMocks();
123
+ progressInstances.length = 0;
124
+ spinnerInstances.length = 0;
125
+ taskLogInstances.length = 0;
126
+ process.env.NODE_ENV = "development";
127
+
128
+ return DITest.create({
129
+ env: "test"
130
+ });
131
+ });
132
+
133
+ afterEach(() => {
134
+ process.env.NODE_ENV = originalNodeEnv;
135
+ return DITest.reset();
136
+ });
137
+
138
+ it("logs start/done using the default log prompt", () => {
139
+ const logger = TaskLogger.from({
140
+ title: "Build project",
141
+ index: 0
142
+ });
143
+
144
+ expect(logger.type).toBe("log");
145
+
146
+ logger.start();
147
+ logger.done();
148
+
149
+ expect(logMock.info).toHaveBeenCalledWith("Build project...");
150
+ expect(logMock.success).toHaveBeenCalledWith("Build project completed");
151
+ });
152
+
153
+ it("emits message when the title changes", () => {
154
+ const logger = TaskLogger.from({
155
+ title: "Initial",
156
+ index: 0
157
+ });
158
+
159
+ logger.title = "Updated title";
160
+
161
+ expect(logMock.message).toHaveBeenCalledWith("Updated title");
162
+ });
163
+
164
+ it("advances the parent progress bar for child progress tasks", () => {
165
+ const parent = new TaskLogger({
166
+ title: "Install dependencies",
167
+ index: 0,
168
+ type: "progress"
169
+ });
170
+ parent.max = 4;
171
+
172
+ const child = TaskLogger.from({
173
+ title: "Install packages",
174
+ index: 1,
175
+ parent
176
+ });
177
+
178
+ child.start();
179
+
180
+ expect(progressInstances[0]?.advance).toHaveBeenCalledWith(25, "Install packages");
181
+ });
182
+
183
+ it("returns the spinner parent when nested spinner tasks are requested", () => {
184
+ const spinnerParent = new TaskLogger({
185
+ title: "Loading spinner",
186
+ index: 0,
187
+ type: "spinner"
188
+ });
189
+
190
+ const nested = TaskLogger.from({
191
+ title: "Nested spinner",
192
+ index: 1,
193
+ parent: spinnerParent
194
+ });
195
+
196
+ expect(nested).toBe(spinnerParent);
197
+ });
198
+
199
+ it("marks taskLog children as skipped via error logger", () => {
200
+ const parent = new TaskLogger({
201
+ title: "Parent log",
202
+ index: 0,
203
+ type: "taskLog"
204
+ });
205
+
206
+ const child = TaskLogger.from({
207
+ title: "Child log",
208
+ index: 1,
209
+ parent
210
+ });
211
+
212
+ child.skip();
213
+
214
+ const childLogger = taskLogInstances.find((instance) => instance.title === "Child log");
215
+ expect(childLogger?.error).toHaveBeenCalledWith("Child log skipped...");
216
+ });
217
+
218
+ it("creates grouped loggers for nested taskLog hierarchies", () => {
219
+ const root = new TaskLogger({
220
+ title: "Root task",
221
+ index: 0,
222
+ type: "taskLog"
223
+ });
224
+ const child = TaskLogger.from({
225
+ title: "Child task",
226
+ index: 1,
227
+ parent: root
228
+ });
229
+
230
+ TaskLogger.from({
231
+ title: "Grandchild task",
232
+ index: 2,
233
+ parent: child
234
+ });
235
+
236
+ const childLogger = taskLogInstances.find((instance) => instance.title === "Child task");
237
+ const grandChildLogger = taskLogInstances.find((instance) => instance.title === "Grandchild task");
238
+
239
+ expect(childLogger?.group).toHaveBeenCalledWith("Grandchild task");
240
+ expect(grandChildLogger).toBeDefined();
241
+ });
242
+
243
+ it("routes verbose output through the context logger", async () => {
244
+ const ctx = DITest.createDIContext();
245
+ let infoSpy!: MockInstance;
246
+ let warnSpy!: MockInstance;
247
+ let errorSpy!: MockInstance;
248
+
249
+ await runInContext(ctx, () => {
250
+ const scopedLogger = contextLogger();
251
+ infoSpy = vi.spyOn(scopedLogger, "info");
252
+ warnSpy = vi.spyOn(scopedLogger, "warn");
253
+ errorSpy = vi.spyOn(scopedLogger, "error");
254
+
255
+ const logger = new TaskLogger({
256
+ title: "Verbose task",
257
+ index: 0,
258
+ type: "log",
259
+ verbose: true
260
+ });
261
+
262
+ logger.message("hello");
263
+ logger.warn("be careful");
264
+ logger.error("boom");
265
+ logger.report("report payload");
266
+
267
+ return logger;
268
+ });
269
+
270
+ expect(infoSpy).toHaveBeenCalledWith({
271
+ state: "MSG",
272
+ title: "Verbose task",
273
+ message: "Verbose task - hello"
274
+ });
275
+ expect(warnSpy).toHaveBeenCalledWith({
276
+ title: "Verbose task",
277
+ message: "Verbose task - be careful"
278
+ });
279
+ expect(errorSpy).toHaveBeenCalledWith({
280
+ title: "Verbose task",
281
+ message: "Verbose task - boom"
282
+ });
283
+ expect(infoSpy).toHaveBeenCalledWith({
284
+ title: "Verbose task",
285
+ message: "report payload"
286
+ });
287
+ });
288
+
289
+ it("does not emit context logger output when NODE_ENV=test", async () => {
290
+ process.env.NODE_ENV = "test";
291
+ const ctx = DITest.createDIContext();
292
+
293
+ await runInContext(ctx, () => {
294
+ const scopedLogger = contextLogger();
295
+ const infoSpy = vi.spyOn(scopedLogger, "info");
296
+
297
+ const logger = new TaskLogger({
298
+ title: "Suppressed task",
299
+ index: 0,
300
+ type: "log",
301
+ verbose: true
302
+ });
303
+
304
+ logger.message("should not show");
305
+ expect(infoSpy).not.toHaveBeenCalled();
306
+ });
307
+ });
308
+
309
+ it("writes raw output through info fallback", () => {
310
+ const logger = TaskLogger.from({
311
+ title: "Output task",
312
+ index: 0
313
+ });
314
+
315
+ logger.output = "custom message";
316
+
317
+ expect(logMock.info).toHaveBeenCalledWith("custom message");
318
+ });
319
+ });
320
+ let TaskLogger: typeof TaskLoggerClass;
321
+
322
+ beforeAll(async () => {
323
+ ({TaskLogger} = await import("./TaskLogger.js"));
324
+ });
@@ -0,0 +1,319 @@
1
+ import {log, progress, spinner, taskLog} from "@clack/prompts";
2
+ import {contextLogger} from "@tsed/di";
3
+
4
+ export interface TaskLoggerOptions {
5
+ title: string;
6
+ index: number;
7
+ type?: "group" | "taskLog" | "log" | "spinner" | "progress";
8
+ parent?: TaskLogger;
9
+ verbose?: boolean;
10
+ }
11
+
12
+ export class TaskLogger {
13
+ readonly type: TaskLoggerOptions["type"];
14
+ public max: number;
15
+ #title: string;
16
+ #logger: any;
17
+ #verbose: boolean | undefined;
18
+ #parent: TaskLogger | undefined;
19
+ #index: number;
20
+
21
+ constructor(opts: TaskLoggerOptions) {
22
+ this.#title = opts.title;
23
+ this.type = opts.type;
24
+ this.#parent = opts.parent;
25
+ this.#verbose = opts.verbose;
26
+
27
+ this.#logger = this.create({
28
+ ...opts,
29
+ verbose: this.#verbose
30
+ });
31
+ }
32
+
33
+ get parent() {
34
+ return this.#parent;
35
+ }
36
+
37
+ get isReady() {
38
+ return !!this.#logger;
39
+ }
40
+
41
+ get title() {
42
+ return this.#title;
43
+ }
44
+
45
+ set title(title: string) {
46
+ if (title) {
47
+ this.#title = title;
48
+ this.#logger.message?.(title);
49
+ }
50
+ }
51
+
52
+ set output(message: string) {
53
+ if (message) {
54
+ (this.#logger.info || this.#logger.message)?.(message);
55
+ }
56
+ }
57
+
58
+ static from(opts: TaskLoggerOptions) {
59
+ if (opts.parent) {
60
+ switch (opts.parent.type) {
61
+ case "progress":
62
+ return new TaskLogger({
63
+ ...opts,
64
+ type: "progress",
65
+ parent: opts.parent
66
+ });
67
+ case "spinner":
68
+ return opts.parent;
69
+ }
70
+ }
71
+
72
+ if (opts.parent) {
73
+ if (opts.parent?.parent) {
74
+ opts.type = opts.type || "group";
75
+ } else {
76
+ opts.type = opts.type || "taskLog";
77
+ }
78
+ }
79
+
80
+ opts.type = opts.type || "log";
81
+
82
+ return new TaskLogger(opts);
83
+ }
84
+
85
+ log(message: string) {
86
+ this.#logger.message(`${this.title} - ${message}`);
87
+ }
88
+
89
+ message(message: string) {
90
+ this.#logger.message(`${this.title} - ${message}`);
91
+ }
92
+
93
+ advance() {
94
+ if (this.isReady && this.type === "progress") {
95
+ if (this.isRaw()) {
96
+ this.info(`${this.title} [${this.#index}/${this.parent!.max}]`);
97
+ } else {
98
+ const it = Math.round((1 / this.parent!.max) * 100);
99
+ this.#logger.advance(it, this.title);
100
+ }
101
+ }
102
+
103
+ return this;
104
+ }
105
+
106
+ start() {
107
+ if (this.isReady) {
108
+ const msg = `${this.title}...`;
109
+ switch (this.type) {
110
+ default:
111
+ this.#logger.message(msg);
112
+ break;
113
+ case "log":
114
+ this.#logger.info(msg);
115
+ break;
116
+ case "progress":
117
+ if (this.isChildProgress()) {
118
+ this.advance();
119
+ } else {
120
+ this.#logger.start(this.title);
121
+ }
122
+ break;
123
+ case "spinner":
124
+ this.#logger.start(this.title);
125
+ break;
126
+ }
127
+ }
128
+
129
+ return this;
130
+ }
131
+
132
+ done() {
133
+ if (this.isReady) {
134
+ const msg = `${this.title} completed`;
135
+
136
+ switch (this.type) {
137
+ default:
138
+ this.#logger.success(msg);
139
+ break;
140
+ case "progress":
141
+ if (!this.isChildProgress()) {
142
+ this.#logger.stop(msg);
143
+ }
144
+ break;
145
+ case "spinner":
146
+ this.#logger.stop(msg);
147
+ break;
148
+ }
149
+ }
150
+
151
+ return this;
152
+ }
153
+
154
+ error(message: string | Error) {
155
+ if (this.isReady) {
156
+ this.#logger.error(`${this.title} - ${message}`);
157
+ }
158
+
159
+ return this;
160
+ }
161
+
162
+ info(message: string) {
163
+ if (this.isReady) {
164
+ switch (this.type) {
165
+ case "log":
166
+ case "taskLog":
167
+ this.#logger.info(`${this.title} - ${message}`);
168
+ break;
169
+ default:
170
+ this.#logger.message(`${this.title} - ${message}`);
171
+ break;
172
+ }
173
+ }
174
+
175
+ return this;
176
+ }
177
+
178
+ warn(message: string) {
179
+ if (this.isReady) {
180
+ switch (this.type) {
181
+ case "log":
182
+ case "taskLog":
183
+ this.#logger.warn(`${this.title} - ${message}`);
184
+ break;
185
+ default:
186
+ this.#logger.message(`${this.title} - WARN ${message}`);
187
+ }
188
+ }
189
+
190
+ return this;
191
+ }
192
+
193
+ skip() {
194
+ if (this.isReady) {
195
+ switch (this.type) {
196
+ default:
197
+ case "log":
198
+ this.#logger.warn(`${this.title} skipped...`);
199
+ break;
200
+ case "group":
201
+ case "taskLog":
202
+ this.#logger.error(`${this.title} skipped...`);
203
+ break;
204
+ case "progress":
205
+ this.advance();
206
+ break;
207
+ case "spinner":
208
+ this.#logger.stop(`${this.title} skipped...`);
209
+ break;
210
+ }
211
+ }
212
+ return this;
213
+ }
214
+
215
+ /**
216
+ * @ddeprecated use info or message instead
217
+ * @param message
218
+ */
219
+ report(message: string) {
220
+ (this.#logger.info || this.#logger.message)?.(message);
221
+ }
222
+
223
+ protected isChildProgress() {
224
+ return this.parent?.type === "progress" && this.type === "progress";
225
+ }
226
+
227
+ protected create(opts: TaskLoggerOptions) {
228
+ const {type, title, parent} = opts;
229
+
230
+ if (this.isRaw()) {
231
+ const success = (message: string) => {
232
+ !this.isEnvTest() &&
233
+ contextLogger()?.info({
234
+ state: "SUCCESS",
235
+ title,
236
+ message
237
+ });
238
+ };
239
+ const start = (message: string) => {
240
+ !this.isEnvTest() &&
241
+ contextLogger()?.info({
242
+ title,
243
+ message
244
+ });
245
+ };
246
+ const defaultLogger = {
247
+ message: (message: string) => {
248
+ !this.isEnvTest() &&
249
+ contextLogger()?.info({
250
+ state: "MSG",
251
+ title,
252
+ message
253
+ });
254
+ },
255
+ stop: success,
256
+ success,
257
+ info: start,
258
+ start,
259
+ warn: (message: string) => {
260
+ !this.isEnvTest() &&
261
+ contextLogger()?.warn({
262
+ title,
263
+ message
264
+ });
265
+ },
266
+ error: (message: string) => {
267
+ !this.isEnvTest() &&
268
+ contextLogger()?.error({
269
+ title,
270
+ message
271
+ });
272
+ }
273
+ };
274
+
275
+ defaultLogger.message("...");
276
+ return defaultLogger;
277
+ }
278
+
279
+ switch (type) {
280
+ case "spinner":
281
+ const spin = spinner();
282
+ spin.message(title);
283
+
284
+ return spin;
285
+ case "progress":
286
+ if (parent && this.isChildProgress()) {
287
+ return parent.#logger;
288
+ }
289
+
290
+ return progress({
291
+ style: "block",
292
+ max: 100
293
+ });
294
+
295
+ case "taskLog":
296
+ return taskLog({
297
+ title: this.title
298
+ });
299
+
300
+ case "log":
301
+ return log;
302
+
303
+ case "group":
304
+ if (!parent || parent.type !== "taskLog") {
305
+ return log;
306
+ }
307
+
308
+ return parent.#logger.group(this.title);
309
+ }
310
+ }
311
+
312
+ private isRaw() {
313
+ return this.#verbose || this.isEnvTest();
314
+ }
315
+
316
+ private isEnvTest() {
317
+ return process.env.NODE_ENV === "test";
318
+ }
319
+ }
@@ -0,0 +1,5 @@
1
+ import {context, ContextLogger, contextLogger} from "@tsed/di";
2
+
3
+ export function taskLogger(): ContextLogger {
4
+ return (context().get("TASK_LOGGER") as ContextLogger | undefined) || contextLogger();
5
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./domain/TaskLogger.js";
2
+ export * from "./fn/taskLogger.js";
3
+ export * from "./interfaces/Task.js";
4
+ export * from "./tasks.js";
@@ -0,0 +1,15 @@
1
+ import type {Observable} from "rxjs";
2
+
3
+ import {TaskLogger, type TaskLoggerOptions} from "../domain/TaskLogger.js";
4
+
5
+ export type MaybePromise<T> = Promise<T> | T;
6
+
7
+ type TaskPredicate = MaybePromise<boolean | string | undefined>;
8
+
9
+ export interface Task<Ctx = any> {
10
+ title: string;
11
+ task: (ctx: Ctx, operation: TaskLogger) => MaybePromise<Task[] | unknown> | Observable<unknown>;
12
+ skip?: boolean | string | ((ctx: Ctx) => TaskPredicate);
13
+ enabled?: boolean | ((ctx: Ctx) => MaybePromise<boolean>);
14
+ type?: TaskLoggerOptions["type"];
15
+ }