diff --git a/package-lock.json b/package-lock.json index 93b00ce..23527ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "commander": "11.1.0", "croner": "8.0.0", "cronstrue": "2.47.0", + "lodash": "4.17.21", "pino": "8.17.2", "pino-pretty": "10.3.1", "zod": "3.22.4" @@ -31,6 +32,7 @@ "@total-typescript/ts-reset": "0.5.1", "@trivago/prettier-plugin-sort-imports": "4.3.0", "@types/lint-staged": "13.3.0", + "@types/lodash": "4.14.202", "@types/uuid": "9.0.7", "@typescript-eslint/eslint-plugin": "6.18.1", "concurrently": "8.2.2", @@ -3743,6 +3745,12 @@ "integrity": "sha512-WxGjVP+rA4OJlEdbZdT9MS9PFKQ7kVPhLn26gC+2tnBWBEFEj/KW+IbFfz6sxdxY5U6V7BvyF+3BzCGsAMHhNg==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, "node_modules/@types/node": { "version": "20.11.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", @@ -10644,8 +10652,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash-es": { "version": "4.17.21", diff --git a/package.json b/package.json index 1ed6a87..0f3927f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "commander": "11.1.0", "croner": "8.0.0", "cronstrue": "2.47.0", + "lodash": "4.17.21", "pino": "8.17.2", "pino-pretty": "10.3.1", "zod": "3.22.4" @@ -44,6 +45,7 @@ "@total-typescript/ts-reset": "0.5.1", "@trivago/prettier-plugin-sort-imports": "4.3.0", "@types/lint-staged": "13.3.0", + "@types/lodash": "4.14.202", "@types/uuid": "9.0.7", "@typescript-eslint/eslint-plugin": "6.18.1", "concurrently": "8.2.2", diff --git a/src/index.test.ts b/src/index.test.ts index 21c5806..5eb955b 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -82,7 +82,7 @@ describe("main", () => { statusText: "OK", type: "basic", url: url.toString(), - text: async () => "", + text: async () => "Some Text", } as Response) ); }); @@ -102,11 +102,7 @@ describe("main", () => { process.cwd() ); - proc = main({ - destination, - fs, - signal: controller.signal, - }); + proc = main({ destination, fs, signal: controller.signal }); await jest.advanceTimersByTimeAsync(0); @@ -119,11 +115,15 @@ describe("main", () => { ]); expect(winner).toBe("timeout"); - expect(destination.logs).toContainEqual({ - level: 30, - time: 1696486441293, - msg: "No CRONs Scheduled", - }); + expect(destination.logs).toStrictEqual([ + { + config: "./vercel.json", + level: 20, + time: 1696486441293, + msg: "Watching Config", + }, + { level: 40, time: 1696486441293, msg: "No CRONs Scheduled" }, + ]); }); it("dry ends the process immediately", async () => { @@ -132,12 +132,7 @@ describe("main", () => { process.cwd() ); - proc = main({ - destination, - fs, - signal: controller.signal, - dry: true, - }); + proc = main({ destination, fs, signal: controller.signal, dry: true }); await jest.advanceTimersByTimeAsync(0); @@ -150,11 +145,15 @@ describe("main", () => { ]); expect(winner).not.toBe("timeout"); - expect(destination.logs).toContainEqual({ - level: 30, - time: 1696486441293, - msg: "No CRONs Scheduled", - }); + expect(destination.logs).toStrictEqual([ + { + config: "./vercel.json", + level: 20, + time: 1696486441293, + msg: "Watching Config", + }, + { level: 40, time: 1696486441293, msg: "No CRONs Scheduled" }, + ]); }); it("executes CRON when schedule passes", async () => { @@ -167,26 +166,29 @@ describe("main", () => { process.cwd() ); - proc = main({ - destination, - fs, - signal: controller.signal, - }); + proc = main({ destination, fs, signal: controller.signal }); await jest.advanceTimersByTimeAsync(0); expect(fetchSpy).not.toHaveBeenCalled(); - expect(destination.logs).toContainEqual({ - level: 30, - msg: "Scheduled /some-api Every second", - time: 1696486441293, - }); + expect(destination.logs).toStrictEqual([ + { + config: "./vercel.json", + level: 20, + time: 1696486441293, + msg: "Watching Config", + }, + { + level: 30, + msg: "Scheduled /some-api Every second", + time: 1696486441293, + }, + ]); destination.clear(); await jest.advanceTimersByTimeAsync(1000); - expect(fetchSpy).toHaveBeenNthCalledWith( - 1, + expect(fetchSpy).toHaveBeenCalledWith( "http://localhost:3000/some-api", expect.objectContaining({ method: "GET", @@ -194,53 +196,262 @@ describe("main", () => { headers: {}, }) ); - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(destination.logs).toContainEqual({ - currentRun: "2023-10-05T06:14:02.000Z", - level: 30, - msg: "Started /some-api Every second", - time: 1696486442000, - }); - expect(destination.logs).toContainEqual({ - currentRun: "2023-10-05T06:14:02.000Z", - level: 30, - msg: "Succeeded /some-api Every second", - status: 200, - text: "", - time: 1696486442000, - }); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Started /some-api Every second", + time: 1696486442000, + }, + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Succeeded /some-api Every second", + status: 200, + text: "Some Text", + time: 1696486442000, + }, + ]); destination.clear(); await jest.advanceTimersByTimeAsync(1000); - expect(fetchSpy).toHaveBeenNthCalledWith( - 2, + expect(fetchSpy).toHaveBeenCalledWith( "http://localhost:3000/some-api", - expect.objectContaining({ - method: "GET", - redirect: "manual", - headers: {}, - }) + expect.objectContaining({}) ); - expect(fetchSpy).toHaveBeenCalledTimes(2); - expect(destination.logs).toContainEqual({ - currentRun: "2023-10-05T06:14:03.000Z", - level: 30, - msg: "Started /some-api Every second", - time: 1696486443000, - }); - expect(destination.logs).toContainEqual({ - currentRun: "2023-10-05T06:14:03.000Z", - level: 30, - msg: "Succeeded /some-api Every second", - status: 200, - text: "", - time: 1696486443000, - }); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:03.000Z", + level: 30, + msg: "Started /some-api Every second", + time: 1696486443000, + }, + { + currentRun: "2023-10-05T06:14:03.000Z", + level: 30, + msg: "Succeeded /some-api Every second", + status: 200, + text: "Some Text", + time: 1696486443000, + }, + ]); }); - it("misses CRON if config changes", async () => { + it("handles multiple schedules", async () => { const { fs } = memfs( + { + "./vercel.json": JSON.stringify({ + crons: [ + { path: "/some-api", schedule: "2,4 * * * * *" }, + { path: "/some-other-api", schedule: "3 * * * * *" }, + ], + }), + }, + process.cwd() + ); + + proc = main({ destination, fs, signal: controller.signal }); + + await jest.advanceTimersByTimeAsync(0); + + expect(fetchSpy).not.toHaveBeenCalled(); + expect(destination.logs).toStrictEqual([ + { + config: "./vercel.json", + level: 20, + time: 1696486441293, + msg: "Watching Config", + }, + { + level: 30, + msg: "Scheduled /some-api At 2 and 4 seconds past the minute", + time: 1696486441293, + }, + { + level: 30, + msg: "Scheduled /some-other-api At 3 seconds past the minute", + time: 1696486441293, + }, + ]); + destination.clear(); + + await jest.advanceTimersByTimeAsync(1000); + + expect(fetchSpy).toHaveBeenCalledWith( + "http://localhost:3000/some-api", + expect.objectContaining({}) + ); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Started /some-api At 2 and 4 seconds past the minute", + time: 1696486442000, + }, + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Succeeded /some-api At 2 and 4 seconds past the minute", + status: 200, + text: "Some Text", + time: 1696486442000, + }, + ]); + destination.clear(); + + await jest.advanceTimersByTimeAsync(1000); + + expect(fetchSpy).toHaveBeenCalledWith( + "http://localhost:3000/some-other-api", + expect.objectContaining({}) + ); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:03.000Z", + level: 30, + msg: "Started /some-other-api At 3 seconds past the minute", + time: 1696486443000, + }, + { + currentRun: "2023-10-05T06:14:03.000Z", + level: 30, + msg: "Succeeded /some-other-api At 3 seconds past the minute", + status: 200, + text: "Some Text", + time: 1696486443000, + }, + ]); + destination.clear(); + + await jest.advanceTimersByTimeAsync(1000); + + expect(fetchSpy).toHaveBeenCalledWith( + "http://localhost:3000/some-api", + expect.objectContaining({}) + ); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:04.000Z", + level: 30, + msg: "Started /some-api At 2 and 4 seconds past the minute", + time: 1696486444000, + }, + { + currentRun: "2023-10-05T06:14:04.000Z", + level: 30, + msg: "Succeeded /some-api At 2 and 4 seconds past the minute", + status: 200, + text: "Some Text", + time: 1696486444000, + }, + ]); + destination.clear(); + }); + + it("reschedules CRONs on file change", async () => { + const { fs, vol } = memfs( + { + "./vercel.json": JSON.stringify({ + crons: [{ path: "/some-api", schedule: "* * * * * *" }], + }), + }, + process.cwd() + ); + + proc = main({ destination, fs, signal: controller.signal }); + + await jest.advanceTimersByTimeAsync(0); + + expect(fetchSpy).not.toHaveBeenCalled(); + expect(destination.logs).toStrictEqual([ + { + config: "./vercel.json", + level: 20, + time: 1696486441293, + msg: "Watching Config", + }, + { + level: 30, + msg: "Scheduled /some-api Every second", + time: 1696486441293, + }, + ]); + destination.clear(); + + vol.fromNestedJSON({ "./vercel.json": JSON.stringify({}) }, process.cwd()); + await jest.advanceTimersByTimeAsync(0); + + expect(destination.logs).toStrictEqual([ + { + config: "./vercel.json", + level: 30, + msg: "Config Changed", + time: 1696486441293, + }, + { level: 40, msg: "No CRONs Scheduled", time: 1696486441293 }, + ]); + destination.clear(); + + await jest.advanceTimersByTimeAsync(100000); + + expect(fetchSpy).not.toHaveBeenCalled(); + expect(destination.logs).toHaveLength(0); + }); + + it("keeps running with malformed config", async () => { + const { fs, vol } = memfs( + { "./vercel.json": JSON.stringify([]) }, + process.cwd() + ); + + proc = main({ destination, fs, signal: controller.signal }); + + await jest.advanceTimersByTimeAsync(0); + + expect(fetchSpy).not.toHaveBeenCalled(); + expect(destination.logs).toStrictEqual([ + { + config: "./vercel.json", + level: 20, + time: 1696486441293, + msg: "Watching Config", + }, + { + level: 60, + msg: "Failed to Schedule CRONs", + time: 1696486441293, + error: { + type: "ZodError", + name: "ZodError", + message: expect.any(String), + stack: expect.any(String), + aggregateErrors: [ + { + code: "invalid_type", + expected: "object", + message: "Expected object, received array", + path: [], + received: "array", + stack: "", + type: "Object", + }, + ], + issues: [ + { + code: "invalid_type", + expected: "object", + message: "Expected object, received array", + path: [], + received: "array", + }, + ], + }, + }, + ]); + destination.clear(); + + vol.fromNestedJSON( { "./vercel.json": JSON.stringify({ crons: [{ path: "/some-api", schedule: "* * * * * *" }], @@ -248,44 +459,275 @@ describe("main", () => { }, process.cwd() ); + await jest.advanceTimersByTimeAsync(0); + + expect(destination.logs).toStrictEqual([ + { + config: "./vercel.json", + level: 30, + msg: "Config Changed", + time: 1696486441293, + }, + { + level: 30, + msg: "Scheduled /some-api Every second", + time: 1696486441293, + }, + ]); + destination.clear(); + + await jest.advanceTimersByTimeAsync(1000); + + expect(fetchSpy).toHaveBeenCalledWith( + "http://localhost:3000/some-api", + expect.objectContaining({}) + ); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Started /some-api Every second", + time: 1696486442000, + }, + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Succeeded /some-api Every second", + status: 200, + text: "Some Text", + time: 1696486442000, + }, + ]); + }); + + it("uses specified config", async () => { + const { fs } = memfs( + { + "./vercel-other.json": JSON.stringify({ + crons: [{ path: "/some-api", schedule: "* * * * * *" }], + }), + }, + process.cwd() + ); proc = main({ destination, fs, - level: "trace", signal: controller.signal, + config: "./vercel-other.json", }); await jest.advanceTimersByTimeAsync(0); + destination.clear(); + await jest.advanceTimersByTimeAsync(1000); - expect(fetchSpy).not.toHaveBeenCalled(); - expect(destination.logs).toContainEqual({ - level: 30, - msg: "Scheduled /some-api Every second", - time: 1696486441293, + expect(fetchSpy).toHaveBeenCalledWith( + "http://localhost:3000/some-api", + expect.objectContaining({}) + ); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Started /some-api Every second", + time: 1696486442000, + }, + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Succeeded /some-api Every second", + status: 200, + text: "Some Text", + time: 1696486442000, + }, + ]); + }); + + it("uses specified url", async () => { + const { fs } = memfs( + { + "./vercel.json": JSON.stringify({ + crons: [{ path: "/some-api", schedule: "* * * * * *" }], + }), + }, + process.cwd() + ); + + proc = main({ + destination, + fs, + signal: controller.signal, + url: "https://my-website.com", }); - destination.clear(); - fs.writeFileSync("./vercel.json", JSON.stringify({})); await jest.advanceTimersByTimeAsync(0); + destination.clear(); + await jest.advanceTimersByTimeAsync(1000); - expect(destination.logs).toContainEqual({ - config: "./vercel.json", - level: 30, - msg: "Config Changed", - time: 1696486441293, - }); - expect(destination.logs).toContainEqual({ - level: 30, - msg: "No CRONs Scheduled", - time: 1696486441293, + expect(fetchSpy).toHaveBeenCalledWith( + "https://my-website.com/some-api", + expect.objectContaining({}) + ); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Started /some-api Every second", + time: 1696486442000, + }, + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Succeeded /some-api Every second", + status: 200, + text: "Some Text", + time: 1696486442000, + }, + ]); + }); + + it("uses specified secret", async () => { + const { fs } = memfs( + { + "./vercel.json": JSON.stringify({ + crons: [{ path: "/some-api", schedule: "* * * * * *" }], + }), + }, + process.cwd() + ); + + proc = main({ + destination, + fs, + signal: controller.signal, + secret: "mock-secret", }); + + await jest.advanceTimersByTimeAsync(0); destination.clear(); + await jest.advanceTimersByTimeAsync(1000); - await jest.advanceTimersByTimeAsync(100000); + expect(fetchSpy).toHaveBeenCalledWith( + "http://localhost:3000/some-api", + expect.objectContaining({ + headers: { Authorization: "Bearer mock-secret" }, + }) + ); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Started /some-api Every second", + time: 1696486442000, + }, + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Succeeded /some-api Every second", + status: 200, + text: "Some Text", + time: 1696486442000, + }, + ]); + }); - expect(fetchSpy).not.toHaveBeenCalled(); - expect(fetchSpy).toHaveBeenCalledTimes(0); - expect(destination.logs).toHaveLength(0); + it("prints not-ok fetch responses", async () => { + const { fs } = memfs( + { + "./vercel.json": JSON.stringify({ + crons: [{ path: "/some-api", schedule: "* * * * * *" }], + }), + }, + process.cwd() + ); + + fetchSpy.mockImplementation( + async (url) => + ({ + headers: {}, + ok: false, + redirected: false, + status: 400, + statusText: "OK", + type: "basic", + url: url.toString(), + text: async () => "Mock Error", + } as Response) + ); + + proc = main({ destination, fs, signal: controller.signal }); + + await jest.advanceTimersByTimeAsync(0); + destination.clear(); + await jest.advanceTimersByTimeAsync(1000); + + expect(fetchSpy).toHaveBeenCalledWith( + "http://localhost:3000/some-api", + expect.objectContaining({}) + ); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Started /some-api Every second", + time: 1696486442000, + }, + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 50, + msg: "Failed /some-api Every second", + status: 400, + text: "Mock Error", + time: 1696486442000, + error: { + message: "Mock Error", + type: "Error", + stack: expect.any(String), + }, + }, + ]); + }); + + it("prints fetch errors", async () => { + const { fs } = memfs( + { + "./vercel.json": JSON.stringify({ + crons: [{ path: "/some-api", schedule: "* * * * * *" }], + }), + }, + process.cwd() + ); + + fetchSpy.mockRejectedValue(new Error("Mock Error")); + + proc = main({ destination, fs, signal: controller.signal }); + + await jest.advanceTimersByTimeAsync(0); + destination.clear(); + await jest.advanceTimersByTimeAsync(1000); + + expect(fetchSpy).toHaveBeenCalledWith( + "http://localhost:3000/some-api", + expect.objectContaining({}) + ); + expect(destination.logs).toStrictEqual([ + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 30, + msg: "Started /some-api Every second", + time: 1696486442000, + }, + { + currentRun: "2023-10-05T06:14:02.000Z", + level: 50, + msg: "Failed /some-api Every second", + time: 1696486442000, + error: { + message: "Mock Error", + type: "Error", + stack: expect.any(String), + }, + }, + ]); }); }); diff --git a/src/index.ts b/src/index.ts index 936f5c3..6930c2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,35 +5,12 @@ import boxen from "boxen"; import chalk from "chalk"; import { Cron } from "croner"; import cronstrue from "cronstrue"; +import { debounce } from "lodash/fp"; import pino from "pino"; +import type { LoggerOptions } from "pino"; import z from "zod"; -// TODO [engine:node@>=20.3.0]: Replace with AbortSignal.any -const anySignal = (signals: Array) => { - const controller = new globalThis.AbortController(); - - const onAbort = () => { - controller.abort(); - - signals.forEach((signal) => { - if (signal?.removeEventListener) { - signal.removeEventListener("abort", onAbort); - } - }); - }; - - signals.forEach((signal) => { - if (signal?.addEventListener) { - signal.addEventListener("abort", onAbort); - } - }); - - if (signals.some((signal) => signal?.aborted)) { - onAbort(); - } - - return controller.signal; -}; +import { anySignal } from "./utils"; export const zOpts = z .object({ @@ -58,7 +35,6 @@ export const zOpts = z export const defaults = { config: "./vercel.json", - level: "info", secret: process.env.CRON_SECRET ?? null, url: "http://localhost:3000", } satisfies z.infer; @@ -74,14 +50,14 @@ export const main = async ({ signal?: AbortSignal; }) => { const { + color, config, dry, ignoreTimestamp, - level, + pretty, secret, url, - color = false, - pretty = false, + level = "debug", } = { ...defaults, ...opts, @@ -113,22 +89,23 @@ export const main = async ({ }, }, }), - }; + } satisfies LoggerOptions; const logger = !destination ? pino(loggerOptions) : pino(loggerOptions, destination); if (logger.isLevelEnabled("info") && pretty) { - // eslint-disable-next-line no-console -- boxen! + /* eslint-disable no-console -- boxen! */ console.log( boxen("▲ Vercel CRON ▲", { borderColor: color ? "magenta" : undefined, borderStyle: "round", - margin: { left: 0, right: 0, top: 0, bottom: 1 }, padding: 1, }) ); + console.log(); + /* eslint-enable no-console */ } logger.trace({ opts }, "Parsed Options"); @@ -159,7 +136,7 @@ export const main = async ({ cronstrue.toString(schedule) )}`; - const cron = Cron(schedule, { timezone: "UTC" }, async () => { + const cron = Cron(schedule, async () => { const runLogger = logger.child({ currentRun: cron.currentRun() }); runLogger.info(`Started ${pathString}`); @@ -178,14 +155,15 @@ export const main = async ({ return; } + const text = await res.text(); if (!res.ok) { runLogger.error( - { status: res.status, error: new Error(await res.text()) }, + { status: res.status, text, error: new Error(text) }, `Failed ${pathString}` ); } else { runLogger.info( - { status: res.status, text: await res.text() }, + { status: res.status, text }, `Succeeded ${pathString}` ); } @@ -199,7 +177,7 @@ export const main = async ({ }); if (!crons.length) { - logger.info("No CRONs Scheduled"); + logger.warn("No CRONs Scheduled"); } return controller.abort.bind(controller); @@ -209,7 +187,7 @@ export const main = async ({ let abortPrevious: (() => void) | undefined; - const handler = (async (eventType, filename) => { + const handler = debounce(0, (async (eventType, filename) => { logger.trace({ eventType, filename }, "fs.watch"); if (abortPrevious) { @@ -222,9 +200,9 @@ export const main = async ({ abortPrevious = await scheduleCrons(); } catch (error) { logger.fatal({ error }, "Failed to Schedule CRONs"); - abortPrevious?.(); + abortPrevious = () => {}; } - }) satisfies Parameters[1]; + }) satisfies Parameters[1]); handler("rename", config); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..6eef69d --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,28 @@ +// Stryker disable all + +// TODO [engine:node@>=20.3.0]: Replace with AbortSignal.any +export const anySignal = (signals: Array) => { + const controller = new globalThis.AbortController(); + + const onAbort = () => { + controller.abort(); + + signals.forEach((signal) => { + if (signal?.removeEventListener) { + signal.removeEventListener("abort", onAbort); + } + }); + }; + + signals.forEach((signal) => { + if (signal?.addEventListener) { + signal.addEventListener("abort", onAbort); + } + }); + + if (signals.some((signal) => signal?.aborted)) { + onAbort(); + } + + return controller.signal; +}; diff --git a/src/vercel-cron.test.ts b/src/vercel-cron.test.ts index ad76084..c7fbef8 100644 --- a/src/vercel-cron.test.ts +++ b/src/vercel-cron.test.ts @@ -10,6 +10,7 @@ import { it, jest, } from "@jest/globals"; +import chalk from "chalk"; // eslint-disable-next-line global-require, @typescript-eslint/no-require-imports, unicorn/prefer-module, @typescript-eslint/no-unused-vars -- HACK We're "including" bin by running a process against the built file so jest won't pick it up with `--findRelatedTests`. const helpJestFindRelatedTests = () => require("./vercel-cron"); @@ -81,18 +82,20 @@ Options: it("prints banner", async () => { const { stderr, stdout } = await exec( - "ts-node ./src/vercel-cron.ts --ignoreTimestamp --dry --no-color", + "ts-node ./src/vercel-cron.ts --ignoreTimestamp --dry", { signal: controller.signal } ); expect(stderr).toBe(""); - expect(stdout).toBe(`╭─────────────────────────╮ -│ │ -│ ▲ Vercel CRON ▲ │ -│ │ -╰─────────────────────────╯ - -INFO: No CRONs Scheduled -`); + expect(stdout).toBe( + `${chalk.magenta("╭─────────────────────────╮")} +${chalk.magenta("│")} ${chalk.magenta("│")} +${chalk.magenta("│")} ▲ Vercel CRON ▲ ${chalk.magenta("│")} +${chalk.magenta("│")} ${chalk.magenta("│")} +${chalk.magenta("╰─────────────────────────╯")} + +${chalk.yellow("WARN")}: ${chalk.cyan(`No CRONs Scheduled`)} +` + ); }); }); diff --git a/src/vercel-cron.ts b/src/vercel-cron.ts index 0aada23..678ad6b 100644 --- a/src/vercel-cron.ts +++ b/src/vercel-cron.ts @@ -34,7 +34,7 @@ import pkg from "../package.json"; .option("--no-pretty", "No pretty printing, just a JSON stream of logs") .addOption( new Option("-l --level ", "Logging Level") - .default(defaults.level) + .default("info") .choices([ "trace", "debug",