diff --git a/src/commands/local.mjs b/src/commands/local.mjs index fb6c763b..ec1b7ef3 100644 --- a/src/commands/local.mjs +++ b/src/commands/local.mjs @@ -17,6 +17,7 @@ async function startLocal(argv) { pull: argv.pull, interval: argv.interval, maxAttempts: argv.maxAttempts, + color: argv.color, }); } diff --git a/src/lib/docker-containers.mjs b/src/lib/docker-containers.mjs index 17bb7d0a..a9f2ff98 100644 --- a/src/lib/docker-containers.mjs +++ b/src/lib/docker-containers.mjs @@ -1,7 +1,9 @@ import { container } from "../cli.mjs"; import { CommandError, SUPPORT_MESSAGE } from "./errors.mjs"; +import { colorize, Format } from "./formatting/colorize.mjs"; const IMAGE_NAME = "fauna/faunadb:latest"; +let color = false; /** * Ensures the container is running @@ -23,8 +25,9 @@ export async function ensureContainerRunning({ pull, interval, maxAttempts, + color: _color, }) { - const logger = container.resolve("logger"); + color = _color; if (pull) { await pullImage(IMAGE_NAME); } @@ -35,7 +38,7 @@ export async function ensureContainerRunning({ hostPort, containerPort, }); - logger.stderr( + stderr( `[StartContainer] Container '${containerName}' started. Monitoring HealthCheck for readiness.`, ); await waitForHealthCheck({ @@ -44,9 +47,7 @@ export async function ensureContainerRunning({ interval, maxAttempts, }); - logger.stderr( - `[ContainerReady] Container '${containerName}' is up and healthy.`, - ); + stderr(`[ContainerReady] Container '${containerName}' is up and healthy.`); } /** @@ -57,8 +58,7 @@ export async function ensureContainerRunning({ */ async function pullImage(imageName) { const docker = container.resolve("docker"); - const logger = container.resolve("logger"); // Dependency injection for logger - logger.stderr(`[PullImage] Pulling image '${imageName}'...\n`); + stderr(`[PullImage] Pulling image '${imageName}'...`); try { const stream = await docker.pull(imageName); @@ -70,12 +70,12 @@ async function pullImage(imageName) { docker.modem.followProgress( stream, (err, output) => { - writePullProgress(layers, numLines); + writePullProgress(layers, numLines, imageName); if (err) { reject(err); } else { // Move to the reserved space for completion message - logger.stderr(`[PullImage] Image '${imageName}' pulled.`); + stderr(`[PullImage] Image '${imageName}' pulled.`); resolve(output); } }, @@ -86,7 +86,7 @@ async function pullImage(imageName) { `${event.id}: ${event.status} ${event.progress || ""}`; } if (Date.now() - lastUpdate > 100) { - numLines = writePullProgress(layers, numLines); + numLines = writePullProgress(layers, numLines, imageName); lastUpdate = Date.now(); } }, @@ -106,19 +106,21 @@ async function pullImage(imageName) { * so that the progress is displayed in the same place with no "flicker". * @param {Object} layers The layers of the image * @param {number} numLines The number of lines to clear and update + * @param {string} imageName The image name * @returns {number} The number of lines written. Pass this value back into * the next call to writePullProgress so that it can update the lines in place. */ -function writePullProgress(layers, numLines) { - const logger = container.resolve("logger"); +function writePullProgress(layers, numLines, imageName) { const stderrStream = container.resolve("stderrStream"); // Clear only the necessary lines and update them in place stderrStream.write(`\x1B[${numLines}A`); numLines = 0; // clear the screen stderrStream.write("\x1B[0J"); + stderr(`[PullImage] Pulling image '${imageName}'...`); + numLines++; Object.values(layers).forEach((line) => { - logger.stderr(line); + stderr(line); numLines++; }); return numLines; @@ -137,8 +139,7 @@ function writePullProgress(layers, numLines) { */ async function findContainer({ containerName, hostPort }) { const docker = container.resolve("docker"); - const logger = container.resolve("logger"); // Dependency injection for logger - logger.stderr(`[FindContainer] Looking for container '${containerName}'...`); + stderr(`[FindContainer] Looking for container '${containerName}'...`); const filters = JSON.stringify({ name: [containerName] }); const containers = await docker.listContainers({ all: true, filters }); if (containers.length === 0) { @@ -253,14 +254,13 @@ async function startContainer({ containerPort, }) { const docker = container.resolve("docker"); - const logger = container.resolve("logger"); const existingContainer = await findContainer({ containerName, hostPort }); let logStream = undefined; if (existingContainer) { const dockerContainer = docker.getContainer(existingContainer.Id); const state = existingContainer.State; if (state === "paused") { - logger.stderr( + stderr( `[StartContainer] Container '${containerName}' exists but is paused. Unpausing it...`, ); await dockerContainer.unpause(); @@ -269,7 +269,7 @@ async function startContainer({ containerName, }); } else if (state === "created" || state === "exited") { - logger.stderr( + stderr( `[StartContainer] Container '${containerName}' exists in state '${existingContainer.State}'. Starting it...`, ); await dockerContainer.start(); @@ -278,7 +278,7 @@ async function startContainer({ containerName, }); } else if (state === "running") { - logger.stderr( + stderr( `[StartContainer] Container '${containerName}' is already running.`, ); } else { @@ -287,7 +287,7 @@ async function startContainer({ ); } } else { - logger.stderr(`[StartContainer] Starting container '${containerName}'...`); + stderr(`[StartContainer] Starting container '${containerName}'...`); const dockerContainer = await createContainer({ imageName, containerName, @@ -312,7 +312,6 @@ async function startContainer({ * @returns {Promise} The log stream */ async function createLogStream({ dockerContainer, containerName }) { - const logger = container.resolve("logger"); let logStream = await dockerContainer.logs({ stdout: true, stderr: true, @@ -322,13 +321,11 @@ async function createLogStream({ dockerContainer, containerName }) { // Pipe the logs to your logger logStream.on("data", (chunk) => { - logger.stderr(`[StartContainer][${containerName}] ${chunk.toString()}`); + stderr(`[StartContainer][${containerName}] ${chunk.toString()}`); }); logStream.on("end", async () => { - logger.stderr( - `[StartContainer] Container '${containerName}' logs have finished.`, - ); + stderr(`[StartContainer] Container '${containerName}' logs have finished.`); logStream = await createLogStream({ dockerContainer, containerName, @@ -336,7 +333,7 @@ async function createLogStream({ dockerContainer, containerName }) { }); logStream.on("error", (error) => { - logger.stderr( + stderr( `[StartContainer] Error tailing logs for container '${containerName}': ${error.message}`, ); }); @@ -360,9 +357,8 @@ async function waitForHealthCheck({ interval = 10000, logStream, }) { - const logger = container.resolve("logger"); const fetch = container.resolve("fetch"); - logger.stderr(`[HealthCheck] Waiting for Fauna to be ready at ${url}...`); + stderr(`[HealthCheck] Waiting for Fauna to be ready at ${url}...`); let attemptCounter = 0; let errorMessage = ""; @@ -374,7 +370,7 @@ async function waitForHealthCheck({ timeout: 1000, }); if (response.ok) { - logger.stderr(`[HealthCheck] Fauna is ready at ${url}`); + stderr(`[HealthCheck] Fauna is ready at ${url}`); logStream?.destroy(); return; } @@ -382,7 +378,7 @@ async function waitForHealthCheck({ } catch (e) { errorMessage = `with error: ${e.message}`; } - logger.stderr( + stderr( `[HealthCheck] Fauna is not yet ready. Attempt ${attemptCounter + 1}/${maxAttempts} failed ${errorMessage}. Retrying in ${interval / 1000} seconds...`, ); attemptCounter++; @@ -392,10 +388,19 @@ async function waitForHealthCheck({ }); } - logger.stderr( + stderr( `[HealthCheck] Max attempts reached. Service at ${url} did not respond.`, ); throw new CommandError( `[HealthCheck] Fauna at ${url} is not ready after ${maxAttempts} attempts. Consider increasing --interval or --maxAttempts.`, ); } + +/** + * Outputs to stderr. + * @param {string} log The log + */ +function stderr(log) { + const logger = container.resolve("logger"); + logger.stderr(colorize(log, { format: Format.LOG, color })); +} diff --git a/src/lib/formatting/codeToAnsi.mjs b/src/lib/formatting/codeToAnsi.mjs index de53f74f..91b7c196 100644 --- a/src/lib/formatting/codeToAnsi.mjs +++ b/src/lib/formatting/codeToAnsi.mjs @@ -2,6 +2,7 @@ import chalk from "chalk"; import { createHighlighterCoreSync } from "shiki/core"; import { createJavaScriptRegexEngine } from "shiki/engine/javascript"; import json from "shiki/langs/json.mjs"; +import log from "shiki/langs/log.mjs"; import githubDarkHighContrast from "shiki/themes/github-dark-high-contrast.mjs"; import { isTTY } from "../misc.mjs"; @@ -12,7 +13,7 @@ const THEME = "github-dark-high-contrast"; export const createHighlighter = () => { const highlighter = createHighlighterCoreSync({ themes: [githubDarkHighContrast], - langs: [json, fql], + langs: [fql, log, json], engine: createJavaScriptRegexEngine(), }); @@ -64,7 +65,7 @@ const { codeToTokensBase, getTheme } = createHighlighter(); * Returns a string with ANSI codes applied to the code. This is a JS port of the * TypeScript codeToAnsi function from the Shiki library. * @param {*} code - The code to format. - * @param {"json" | "fql"} language - The language of the code. + * @param {"fql" | "log" | "json"} language - The language of the code. * @returns {string} - The formatted code with ANSI codes applied. */ export function codeToAnsi(code, language) { diff --git a/src/lib/formatting/colorize.mjs b/src/lib/formatting/colorize.mjs index 30fb1809..a162cc54 100644 --- a/src/lib/formatting/colorize.mjs +++ b/src/lib/formatting/colorize.mjs @@ -1,9 +1,11 @@ import stripAnsi from "strip-ansi"; import { container } from "../../cli.mjs"; +import { codeToAnsi } from "./codeToAnsi.mjs"; export const Format = { FQL: "fql", + LOG: "log", JSON: "json", TEXT: "text", }; @@ -42,6 +44,14 @@ const jsonToAnsi = (obj) => { return res.trim(); }; +const logToAnsi = (obj) => { + if (typeof obj !== "string") { + throw new Error("Unable to format LOG unless it is already a string."); + } + const res = codeToAnsi(obj, "log"); + return res.trim(); +}; + /** * Formats an object for display with ANSI color codes. * @param {any} obj - The object to format @@ -55,6 +65,8 @@ export const toAnsi = (obj, { format = Format.TEXT } = {}) => { return fqlToAnsi(obj); case Format.JSON: return jsonToAnsi(obj); + case Format.LOG: + return logToAnsi(obj); default: return textToAnsi(obj); } diff --git a/test/local.mjs b/test/local.mjs index 72f22a04..66c7abc4 100644 --- a/test/local.mjs +++ b/test/local.mjs @@ -87,7 +87,7 @@ describe("ensureContainerRunning", () => { docker.listContainers.onCall(0).resolves([]); try { // Run the actual command - await run("local", container); + await run("local --no-color", container); throw new Error("Expected an error to be thrown."); } catch (_) { // Expected error, no action needed @@ -120,7 +120,7 @@ Please pass a --hostPort other than '8443'.", logs: logsStub, unpause: unpauseStub, }); - await run("local", container); + await run("local --no-color", container); expect(unpauseStub).not.to.have.been.called; expect(startStub).to.have.been.called; expect(logsStub).to.have.been.calledWith({ @@ -169,7 +169,7 @@ Please pass a --hostPort other than '8443'.", unpause: unpauseStub, }); await run( - "local --hostPort 10 --containerPort 11 --name Taco --hostIp 127.0.0.1", + "local --no-color --hostPort 10 --containerPort 11 --name Taco --hostIp 127.0.0.1", container, ); expect(docker.createContainer).to.have.been.calledWith({ @@ -204,7 +204,7 @@ Please pass a --hostPort other than '8443'.", logs: logsStub, unpause: unpauseStub, }); - await run("local --pull false", container); + await run("local --no-color --pull false", container); expect(docker.pull).not.to.have.been.called; expect(docker.modem.followProgress).not.to.have.been.called; expect(startStub).to.have.been.called; @@ -229,7 +229,7 @@ Please pass a --hostPort other than '8443'.", unpause: unpauseStub, }); try { - await run("local", container); + await run("local --no-color", container); throw new Error("Expected an error to be thrown."); } catch (_) {} expect(docker.pull).to.have.been.called; @@ -271,7 +271,7 @@ https://support.fauna.com/hc/en-us/requests/new`, fetch.onCall(0).rejects(); fetch.resolves(f({}, 503)); // fail from http try { - await run("local --interval 0 --maxAttempts 3", container); + await run("local --no-color --interval 0 --maxAttempts 3", container); throw new Error("Expected an error to be thrown."); } catch (_) {} const written = stderrStream.getWritten(); @@ -307,7 +307,7 @@ https://support.fauna.com/hc/en-us/requests/new`, unpause: unpauseStub, }); try { - await run("local", container); + await run("local --no-color", container); throw new Error("Expected an error to be thrown."); } catch (_) {} const written = stderrStream.getWritten(); @@ -319,7 +319,7 @@ https://support.fauna.com/hc/en-us/requests/new`, it("throws an error if interval is less than 0", async () => { try { - await run("local --interval -1", container); + await run("local --no-color --interval -1", container); throw new Error("Expected an error to be thrown."); } catch (_) {} const written = stderrStream.getWritten(); @@ -332,7 +332,7 @@ https://support.fauna.com/hc/en-us/requests/new`, it("throws an error if maxAttempts is less than 1", async () => { try { - await run("local --maxAttempts 0", container); + await run("local --no-color --maxAttempts 0", container); throw new Error("Expected an error to be thrown."); } catch (_) {} const written = stderrStream.getWritten(); @@ -417,7 +417,7 @@ https://support.fauna.com/hc/en-us/requests/new`, unpause: unpauseStub, }); try { - await run("local", container); + await run("local --no-color", container); } catch (_) { expect(test.state).to.equal("dead"); } @@ -433,7 +433,7 @@ https://support.fauna.com/hc/en-us/requests/new`, test.expectCalls(); expect(logger.stderr).to.have.been.calledWith(test.startMessage); expect(logger.stderr).to.have.been.calledWith( - `[PullImage] Pulling image 'fauna/faunadb:latest'...\n`, + `[PullImage] Pulling image 'fauna/faunadb:latest'...`, ); expect(logger.stderr).to.have.been.calledWith( "[PullImage] Image 'fauna/faunadb:latest' pulled.",