diff --git a/.changeset/polite-gifts-judge.md b/.changeset/polite-gifts-judge.md new file mode 100644 index 0000000..db26796 --- /dev/null +++ b/.changeset/polite-gifts-judge.md @@ -0,0 +1,5 @@ +--- +"@arethetypeswrong/cli": patch +--- + +Fix truncated stdout when piping more than 64kb to another process diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index b17786c..d62212e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -16,6 +16,8 @@ import * as render from "./render/index.js"; import { major, minor } from "semver"; import { getExitCode } from "./getExitCode.js"; import { applyProfile, profiles } from "./profiles.js"; +import { write } from "./write.js"; +import { Writable } from "stream"; const packageJson = createRequire(import.meta.url)("../package.json"); const version = packageJson.version; @@ -98,8 +100,13 @@ particularly ESM-related module resolution issues.`, applyProfile(opts.profile, opts); } + let out: Writable = process.stdout; if (opts.quiet) { - console.log = () => {}; + out = new (class extends Writable { + _write(_chunk: any, _encoding: BufferEncoding, callback: (error?: Error | null) => void) { + callback(); + } + })(); } if (!opts.color) { @@ -223,7 +230,7 @@ particularly ESM-related module resolution issues.`, result.problems = groupProblemsByKind(analysis.problems); } - console.log(JSON.stringify(result, undefined, 2)); + await write(JSON.stringify(result, undefined, 2), out); if (deleteTgz) { await unlink(deleteTgz); @@ -237,12 +244,12 @@ particularly ESM-related module resolution issues.`, return; } - console.log(); + await write("", out); if (analysis.types) { - console.log(await render.typed(analysis, opts)); + await write(await render.typed(analysis, opts), out); process.exitCode = getExitCode(analysis, opts); } else { - console.log(render.untyped(analysis as core.UntypedResult)); + await write(render.untyped(analysis as core.UntypedResult), out); } if (deleteTgz) { diff --git a/packages/cli/src/write.ts b/packages/cli/src/write.ts new file mode 100644 index 0000000..9e00a8f --- /dev/null +++ b/packages/cli/src/write.ts @@ -0,0 +1,27 @@ +import { Readable, Writable } from "node:stream"; + +// JSON output is often longer than 64 kb, so we need to use streams to write it to stdout +// in order to avoid truncation when piping to other commands. +export async function write(data: string, out: Writable): Promise { + return new Promise((resolve, reject) => { + const stream = new Readable({ + read() { + this.push(data); + this.push("\n"); + this.push(null); + }, + }); + + stream.on("data", (chunk) => { + out.write(chunk); + }); + + stream.on("end", () => { + resolve(); + }); + + out.on("error", (err) => { + reject(err); + }); + }); +} diff --git a/packages/cli/test/snapshots.test.ts b/packages/cli/test/snapshots.test.ts index 6e52676..e01816a 100644 --- a/packages/cli/test/snapshots.test.ts +++ b/packages/cli/test/snapshots.test.ts @@ -1,5 +1,5 @@ +import { execFileSync, type SpawnSyncReturns } from "child_process"; import { access, readFile, writeFile } from "fs/promises"; -import { execSync, type SpawnSyncReturns } from "child_process"; import assert from "node:assert"; import path from "node:path"; import { after, describe, test } from "node:test"; @@ -10,7 +10,7 @@ function resolveFileRelativePath(relPath: string) { return path.resolve(directoryPath, relPath); } -const attw = `node ${resolveFileRelativePath("../../dist/index.js")}`; +const attw = resolveFileRelativePath("../../dist/index.js"); const updateSnapshots = process.env.UPDATE_SNAPSHOTS || process.env.U; const testFilter = (process.env.TEST_FILTER || process.env.T)?.toLowerCase(); @@ -91,7 +91,8 @@ describe("snapshots", async () => { let stderr = ""; let exitCode = 0; try { - stdout = execSync(`${attw} ${tarballPath} ${options ?? defaultOpts}`, { + stdout = execFileSync(process.execPath, [attw, tarballPath, ...(options ?? defaultOpts).split(" ")], { + maxBuffer: 1024 * 1024 * 1024, encoding: "utf8", env: { ...process.env, FORCE_COLOR: "0" }, });