From e7202d63e715728bc891eab0075eddc6194980db Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Wed, 23 Apr 2025 23:42:19 +0800 Subject: [PATCH] Refactor mockable functionalities (#17392) --- src/cli/format.js | 24 ++-- src/cli/index.js | 6 +- src/cli/is-tty.js | 8 -- src/cli/logger.js | 29 +---- src/cli/mockable.js | 17 +++ src/cli/prettier-internal.js | 1 - src/cli/utilities/clear-stream-text.js | 28 +++++ src/common/mockable.js | 18 +-- src/index.js | 6 +- src/utils/create-mockable.js | 42 +++++++ .../line-after-filepath-with-errors.js.snap | 11 +- .../__snapshots__/piped-output.js.snap | 10 +- .../__snapshots__/print-code.js.snap | 4 +- tests/integration/__tests__/cli.js | 6 +- tests/integration/__tests__/mockable.js | 13 ++- tests/integration/cli-worker.js | 107 +++++++++++------- tests/integration/env.js | 14 ++- tests/integration/run-cli.js | 98 +++++++++------- 18 files changed, 280 insertions(+), 162 deletions(-) delete mode 100644 src/cli/is-tty.js create mode 100644 src/cli/mockable.js create mode 100644 src/cli/utilities/clear-stream-text.js create mode 100644 src/utils/create-mockable.js diff --git a/src/cli/format.js b/src/cli/format.js index 30abf3d50..eedeb4c9b 100644 --- a/src/cli/format.js +++ b/src/cli/format.js @@ -5,19 +5,16 @@ import * as prettier from "../index.js"; import { expandPatterns } from "./expand-patterns.js"; import findCacheFile from "./find-cache-file.js"; import FormatResultsCache from "./format-results-cache.js"; -import isTTY from "./is-tty.js"; +import mockable from "./mockable.js"; import getOptionsForFile from "./options/get-options-for-file.js"; import { createIsIgnoredFunction, createTwoFilesPatch, errors, - mockable, picocolors, } from "./prettier-internal.js"; import { normalizeToPosix, statSafe } from "./utils.js"; -const { writeFormattedFile, getTimestamp } = mockable; - function diff(a, b) { return createTwoFilesPatch("", "", a, b, "", "", { context: 2 }); } @@ -33,7 +30,7 @@ function handleError(context, filename, error, printedFilename, ignoreUnknown) { error instanceof errors.UndefinedParserError; if (printedFilename) { - // Can't test on CI, `isTTY()` is always false, see ./is-tty.js + // Can't test on CI, `isTTY` is always false, see comments in `formatFiles` /* c8 ignore next 3 */ if ((context.argv.write || ignoreUnknown) && errorIsUndefinedParseError) { printedFilename.clear(); @@ -193,11 +190,11 @@ async function format(context, input, opt) { context.logger.debug( `'${performanceTestFlag.name}' found, running formatWithCursor ${repeat} times.`, ); - const start = getTimestamp(); + const start = mockable.getTimestamp(); for (let i = 0; i < repeat; ++i) { await prettier.formatWithCursor(input, opt); } - const averageMs = (getTimestamp() - start) / repeat; + const averageMs = (mockable.getTimestamp() - start) / repeat; const results = { repeat, hz: 1000 / averageMs, @@ -303,6 +300,11 @@ async function formatFiles(context) { } } + // Some CI pipelines incorrectly report process.stdout.isTTY status, + // which causes unwanted lines in the output. An additional check for isCI() helps. + // See https://github.com/prettier/prettier/issues/5801 + const isTTY = mockable.isStreamTTY(process.stdout) && !mockable.isCI(); + for await (const { error, filename, ignoreUnknown } of expandPatterns( context, )) { @@ -331,7 +333,7 @@ async function formatFiles(context) { const fileNameToDisplay = normalizeToPosix(path.relative(cwd, filename)); let printedFilename; - if (isTTY()) { + if (isTTY) { printedFilename = context.logger.log(fileNameToDisplay, { newline: false, clearable: true, @@ -363,7 +365,7 @@ async function formatFiles(context) { continue; } - const start = getTimestamp(); + const start = mockable.getTimestamp(); const isCacheExists = formatResultsCache?.existsAvailableFormatResultsCache( filename, @@ -405,7 +407,7 @@ async function formatFiles(context) { } if (context.argv.write) { - const timeToDisplay = `${Math.round(getTimestamp() - start)}ms`; + const timeToDisplay = `${Math.round(mockable.getTimestamp() - start)}ms`; // Don't write the file if it won't change in order not to invalidate // mtime based caches. if (isDifferent) { @@ -414,7 +416,7 @@ async function formatFiles(context) { } try { - await writeFormattedFile(filename, output); + await mockable.writeFormattedFile(filename, output); // Set cache if format succeeds shouldSetCache = true; diff --git a/src/cli/index.js b/src/cli/index.js index 81e6ef407..9335874ef 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -4,6 +4,7 @@ import logFileInfoOrDie from "./file-info.js"; import logResolvedConfigPathOrDie from "./find-config-path.js"; import { formatFiles, formatStdin } from "./format.js"; import createLogger from "./logger.js"; +import mockable from "./mockable.js"; import { parseArgvWithoutPlugins } from "./options/parse-cli-arguments.js"; import printSupportInfo from "./print-support-info.js"; import { createDetailedUsage, createUsage } from "./usage.js"; @@ -88,7 +89,8 @@ async function main(context) { const hasFilePatterns = context.filePatterns.length > 0; const useStdin = - !hasFilePatterns && (!process.stdin.isTTY || context.argv.filepath); + !hasFilePatterns && + (!mockable.isStreamTTY(process.stdin) || context.argv.filepath); if (useStdin) { if (context.argv.cache) { @@ -109,3 +111,5 @@ async function main(context) { } export { run }; +// Exposed for tests +export { mockable } from "./mockable.js"; diff --git a/src/cli/is-tty.js b/src/cli/is-tty.js deleted file mode 100644 index fc8e9bca0..000000000 --- a/src/cli/is-tty.js +++ /dev/null @@ -1,8 +0,0 @@ -import { mockable } from "./prettier-internal.js"; - -// Some CI pipelines incorrectly report process.stdout.isTTY status, -// which causes unwanted lines in the output. An additional check for isCI() helps. -// See https://github.com/prettier/prettier/issues/5801 -export default function isTTY() { - return process.stdout.isTTY && !mockable.isCI(); -} diff --git a/src/cli/logger.js b/src/cli/logger.js index 1eab35dee..ba59caa90 100644 --- a/src/cli/logger.js +++ b/src/cli/logger.js @@ -1,31 +1,6 @@ -import readline from "node:readline"; -import stripAnsi from "strip-ansi"; -import wcwidth from "wcwidth.js"; +import mockable from "./mockable.js"; import { picocolors } from "./prettier-internal.js"; -const countLines = (stream, text) => { - const columns = stream.columns || 80; - let lineCount = 0; - for (const line of stripAnsi(text).split("\n")) { - lineCount += Math.max(1, Math.ceil(wcwidth(line) / columns)); - } - return lineCount; -}; - -const clear = (stream, text) => () => { - const lineCount = countLines(stream, text); - - for (let line = 0; line < lineCount; line++) { - /* c8 ignore next 3 */ - if (line > 0) { - readline.moveCursor(stream, 0, -1); - } - - readline.clearLine(stream, 0); - readline.cursorTo(stream, 0); - } -}; - const emptyLogResult = { clear() {} }; function createLogger(logLevel = "log") { return { @@ -56,7 +31,7 @@ function createLogger(logLevel = "log") { if (options.clearable) { return { - clear: clear(stream, message), + clear: () => mockable.clearStreamText(stream, message), }; } }; diff --git a/src/cli/mockable.js b/src/cli/mockable.js new file mode 100644 index 000000000..804da58fd --- /dev/null +++ b/src/cli/mockable.js @@ -0,0 +1,17 @@ +import fs from "node:fs/promises"; +import { performance } from "node:perf_hooks"; +import { isCI } from "ci-info"; +// @ts-expect-error +import { __internal as sharedWithCli } from "../index.js"; +import clearStreamText from "./utilities/clear-stream-text.js"; + +const mockable = sharedWithCli.utils.createMockable({ + clearStreamText, + getTimestamp: performance.now.bind(performance), + isCI: () => isCI, + isStreamTTY: (stream) => stream.isTTY, + writeFormattedFile: (file, data) => fs.writeFile(file, data), +}); + +export default mockable.mocked; +export { mockable }; diff --git a/src/cli/prettier-internal.js b/src/cli/prettier-internal.js index ef7d83d61..055114054 100644 --- a/src/cli/prettier-internal.js +++ b/src/cli/prettier-internal.js @@ -12,7 +12,6 @@ export const { vnopts, fastGlob, createTwoFilesPatch, - mockable, picocolors, leven, } = sharedWithCli; diff --git a/src/cli/utilities/clear-stream-text.js b/src/cli/utilities/clear-stream-text.js new file mode 100644 index 000000000..36c8355f2 --- /dev/null +++ b/src/cli/utilities/clear-stream-text.js @@ -0,0 +1,28 @@ +import readline from "node:readline"; +import stripAnsi from "strip-ansi"; +import wcwidth from "wcwidth.js"; + +const countLines = (stream, text) => { + const columns = stream.columns || 80; + let lineCount = 0; + for (const line of stripAnsi(text).split("\n")) { + lineCount += Math.max(1, Math.ceil(wcwidth(line) / columns)); + } + return lineCount; +}; + +function clearStreamText(stream, text) { + const lineCount = countLines(stream, text); + + for (let line = 0; line < lineCount; line++) { + /* c8 ignore next 3 */ + if (line > 0) { + readline.moveCursor(stream, 0, -1); + } + + readline.clearLine(stream, 0); + readline.cursorTo(stream, 0); + } +} + +export default clearStreamText; diff --git a/src/common/mockable.js b/src/common/mockable.js index f57bf9479..cbf68566c 100644 --- a/src/common/mockable.js +++ b/src/common/mockable.js @@ -1,16 +1,8 @@ -import fs from "node:fs/promises"; -import { performance } from "node:perf_hooks"; -import { isCI } from "ci-info"; +import createMockable from "../utils/create-mockable.js"; -function writeFormattedFile(file, data) { - return fs.writeFile(file, data); -} - -const mockable = { +const mockable = createMockable({ getPrettierConfigSearchStopDirectory: () => undefined, - isCI: () => isCI, - writeFormattedFile, - getTimestamp: performance.now.bind(performance), -}; +}); -export default mockable; +export default mockable.mocked; +export { mockable }; diff --git a/src/index.js b/src/index.js index 6d88aa219..cf06e88c9 100644 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ import picocolors from "picocolors"; import * as vnopts from "vnopts"; import * as errors from "./common/errors.js"; import getFileInfoWithoutPlugins from "./common/get-file-info.js"; -import mockable from "./common/mockable.js"; +import { mockable } from "./common/mockable.js"; import { clearCache as clearConfigCache, resolveConfig, @@ -31,6 +31,7 @@ import { getSupportInfo as getSupportInfoWithoutPlugins, normalizeOptionSettings, } from "./main/support.js"; +import createMockable from "./utils/create-mockable.js"; import { createIsIgnoredFunction } from "./utils/ignore.js"; import inferParserWithoutPlugins from "./utils/infer-parser.js"; import omit from "./utils/object-omit.js"; @@ -114,8 +115,8 @@ const sharedWithCli = { leven, utils: { omit, + createMockable, }, - mockable, }; const debugApis = { @@ -124,6 +125,7 @@ const debugApis = { formatDoc: withPlugins(core.formatDoc), printToDoc: withPlugins(core.printToDoc), printDocToString: withPlugins(core.printDocToString), + // Exposed for tests mockable, }; diff --git a/src/utils/create-mockable.js b/src/utils/create-mockable.js new file mode 100644 index 000000000..4f7b43bbb --- /dev/null +++ b/src/utils/create-mockable.js @@ -0,0 +1,42 @@ +/** +@template {{ [key: string]: any }} T +@template {keyof T} K +@param {T} implementations +@returns {{ + mocked: T, + implementations: T, + mockImplementation(functionality: K, implementation: T[K]): void, + mockImplementations(overrideImplementations: T): void, + mockRestore(): void, +}} +*/ +function createMockable(implementations) { + const mocked = { ...implementations }; + const mockImplementation = (functionality, implementation) => { + if (!Object.hasOwn(implementations, functionality)) { + throw new Error(`Unexpected mock '${functionality}'.`); + } + // @ts-expect-error -- ?? + mocked[functionality] = implementation; + }; + const mockImplementations = (overrideImplementations) => { + for (const [functionality, implementation] of Object.entries( + overrideImplementations, + )) { + mockImplementation(functionality, implementation); + } + }; + const mockRestore = () => { + Object.assign(mocked, implementations); + }; + + return { + mocked, + implementations, + mockImplementation, + mockImplementations, + mockRestore, + }; +} + +export default createMockable; diff --git a/tests/integration/__tests__/__snapshots__/line-after-filepath-with-errors.js.snap b/tests/integration/__tests__/__snapshots__/line-after-filepath-with-errors.js.snap index b65d80f0b..f311c79c0 100644 --- a/tests/integration/__tests__/__snapshots__/line-after-filepath-with-errors.js.snap +++ b/tests/integration/__tests__/__snapshots__/line-after-filepath-with-errors.js.snap @@ -15,7 +15,7 @@ exports[`Line breaking after filepath with errors 1`] = ` invalid-2.js invalid-2.unknown valid-1.js -[[called readline.clearLine(process.stdout)]] +[[Clear text(process.stdout): valid-1.js]] function foo() {}", "write": [], } @@ -36,8 +36,7 @@ exports[`Line breaking after filepath with errors 2`] = ` invalid-2.js invalid-2.unknown valid-1.js -[[called readline.clearLine(process.stdout)]] -", +[[Clear text(process.stdout): valid-1.js]]", "write": [], } `; @@ -58,7 +57,7 @@ invalid-1.js invalid-2.js invalid-2.unknown valid-1.js -[[called readline.clearLine(process.stdout)]] +[[Clear text(process.stdout): valid-1.js]] All matched files use Prettier code style!", "write": [], } @@ -78,9 +77,9 @@ exports[`Line breaking after filepath with errors 4`] = ` "stdout": "invalid-1.js invalid-2.js invalid-2.unknown -[[called readline.clearLine(process.stdout)]] +[[Clear text(process.stdout): invalid-2.unknown]] valid-1.js -[[called readline.clearLine(process.stdout)]] +[[Clear text(process.stdout): valid-1.js]] valid-1.js 0ms (unchanged)", "write": [], } diff --git a/tests/integration/__tests__/__snapshots__/piped-output.js.snap b/tests/integration/__tests__/__snapshots__/piped-output.js.snap index b8d94e566..238b7c087 100644 --- a/tests/integration/__tests__/__snapshots__/piped-output.js.snap +++ b/tests/integration/__tests__/__snapshots__/piped-output.js.snap @@ -5,7 +5,7 @@ exports[`no file diffs with --check + formatted file 1`] = ` "stderr": "", "stdout": "Checking formatting... formatted.js -[[called readline.clearLine(process.stdout)]] +[[Clear text(process.stdout): formatted.js]] All matched files use Prettier code style!", "write": [], } @@ -24,8 +24,7 @@ exports[`no file diffs with --list-different + formatted file 1`] = ` { "stderr": "", "stdout": "formatted.js -[[called readline.clearLine(process.stdout)]] -", +[[Clear text(process.stdout): formatted.js]]", "write": [], } `; @@ -44,8 +43,7 @@ exports[`output with --check + unformatted differs when piped 1`] = ` [warn] Code style issues fixed in the above file.", "stdout": "Checking formatting... unformatted.js -[[called readline.clearLine(process.stdout)]] -", +[[Clear text(process.stdout): unformatted.js]]", "write": [ { "content": "var x = 1; @@ -75,7 +73,7 @@ exports[`output with --list-different + unformatted differs when piped 1`] = ` { "stderr": "", "stdout": "unformatted.js -[[called readline.clearLine(process.stdout)]] +[[Clear text(process.stdout): unformatted.js]] unformatted.js", "write": [ { diff --git a/tests/integration/__tests__/__snapshots__/print-code.js.snap b/tests/integration/__tests__/__snapshots__/print-code.js.snap index 132bca179..c2f8e2d43 100644 --- a/tests/integration/__tests__/__snapshots__/print-code.js.snap +++ b/tests/integration/__tests__/__snapshots__/print-code.js.snap @@ -3,7 +3,7 @@ exports[`Line breaking after filepath with errors 1`] = ` { "stdout": "ignored.js -[[called readline.clearLine(process.stdout)]] +[[Clear text(process.stdout): ignored.js]] foo( )", } @@ -12,7 +12,7 @@ foo( exports[`Line breaking after filepath with errors 2`] = ` { "stdout": "not-ignored.js -[[called readline.clearLine(process.stdout)]] +[[Clear text(process.stdout): not-ignored.js]] foo();", } `; diff --git a/tests/integration/__tests__/cli.js b/tests/integration/__tests__/cli.js index c60790b54..3f442e738 100644 --- a/tests/integration/__tests__/cli.js +++ b/tests/integration/__tests__/cli.js @@ -1,8 +1,10 @@ import fs from "node:fs"; -import { prettierCli } from "../env.js"; +import { prettierCliEntry } from "../env.js"; describe("CLI", () => { test("CLI should be executable.", () => { - expect(() => fs.accessSync(prettierCli, fs.constants.X_OK)).not.toThrow(); + expect(() => + fs.accessSync(prettierCliEntry, fs.constants.X_OK), + ).not.toThrow(); }); }); diff --git a/tests/integration/__tests__/mockable.js b/tests/integration/__tests__/mockable.js index 68b5f53ea..c4e34e484 100644 --- a/tests/integration/__tests__/mockable.js +++ b/tests/integration/__tests__/mockable.js @@ -1,7 +1,12 @@ -import prettier from "../../config/prettier-entry.js"; +import url from "node:url"; +import { prettierCliMockableEntry } from "../env.js"; -const { mockable } = prettier.__debug; +async function getCliMockable() { + const cli = await import(url.pathToFileURL(prettierCliMockableEntry)); + return cli.mockable; +} -test("isCI", () => { - expect(typeof mockable.isCI()).toBe("boolean"); +test("isCI", async () => { + const mockable = await getCliMockable(); + expect(typeof mockable.implementations.isCI()).toBe("boolean"); }); diff --git a/tests/integration/cli-worker.js b/tests/integration/cli-worker.js index a4433839b..a70c802bb 100644 --- a/tests/integration/cli-worker.js +++ b/tests/integration/cli-worker.js @@ -1,7 +1,10 @@ import path from "node:path"; -import readline from "node:readline"; import url from "node:url"; -import { prettierCli, prettierMainEntry } from "./env.js"; +import { + prettierCliEntry, + prettierCliMockableEntry, + prettierMainEntry, +} from "./env.js"; const normalizeToPosix = path.sep === "\\" @@ -17,55 +20,83 @@ const replaceAll = (text, find, replacement) => ? text.replaceAll(find, replacement) : text.split(find).join(replacement); -async function run(options) { - readline.clearLine = (stream) => { - stream.write( - `\n[[called readline.clearLine(${ +async function getApiMockable() { + const prettier = await import(url.pathToFileURL(prettierMainEntry)); + return prettier.__debug.mockable; +} + +async function getCliMockable() { + const cli = await import(url.pathToFileURL(prettierCliMockableEntry)); + return cli.mockable; +} + +async function mockImplementations(options) { + const [apiMockable, cliMockable] = await Promise.all([ + getApiMockable(), + getCliMockable(), + ]); + + cliMockable.mockImplementations({ + clearStreamText(stream, text) { + const streamName = stream === process.stdout ? "process.stdout" : stream === process.stderr ? "process.stderr" - : "unknown stream" - })]]\n`, - ); - }; + : "unknown stream"; + stream.write(`\n[[Clear text(${streamName}): ${text}]]\n`); + }, + // Time measure in format test + getTimestamp: () => 0, + isCI: () => Boolean(options.ci), + isStreamTTY: (stream) => + stream === process.stdin + ? Boolean(options.isTTY) + : stream === process.stdout + ? Boolean(options.stdoutIsTTY) + : stream.isTTY, + // eslint-disable-next-line require-await + async writeFormattedFile(filename, content) { + filename = normalizeToPosix(path.relative(process.cwd(), filename)); + if ( + options.mockWriteFileErrors && + hasOwn(options.mockWriteFileErrors, filename) + ) { + throw new Error( + options.mockWriteFileErrors[filename] + " (mocked error)", + ); + } + process.send({ + type: "cli:write-file", + data: { filename, content }, + }); + }, + }); - process.stdin.isTTY = Boolean(options.isTTY); - process.stdout.isTTY = Boolean(options.stdoutIsTTY); + apiMockable.mockImplementations({ + getPrettierConfigSearchStopDirectory: () => + url.fileURLToPath(new URL("./cli", import.meta.url)), + }); +} - const prettier = await import(url.pathToFileURL(prettierMainEntry)); - const { mockable } = prettier.__debug; +async function run(options) { + await mockImplementations(options); - // Time measure in format test - mockable.getTimestamp = () => 0; - mockable.isCI = () => Boolean(options.ci); - mockable.getPrettierConfigSearchStopDirectory = () => - url.fileURLToPath(new URL("./cli", import.meta.url)); - // eslint-disable-next-line require-await - mockable.writeFormattedFile = async (filename, content) => { - filename = normalizeToPosix(path.relative(process.cwd(), filename)); - if ( - options.mockWriteFileErrors && - hasOwn(options.mockWriteFileErrors, filename) - ) { - throw new Error( - options.mockWriteFileErrors[filename] + " (mocked error)", - ); - } - - process.send({ - action: "write-file", - data: { filename, content }, - }); - }; - - const { __promise: promise } = await import(url.pathToFileURL(prettierCli)); + const { __promise: promise } = await import( + url.pathToFileURL(prettierCliEntry) + ); await promise; } process.once("message", async (data) => { try { await run(data); + } catch (error) { + // For easier debugging + // eslint-disable-next-line no-console + console.error(error); + process.send({ type: "worker:fault", error }); + throw error; } finally { // On MacOS, if we exit too quick the stdio won't received on main thread if (process.platform === "darwin") { diff --git a/tests/integration/env.js b/tests/integration/env.js index fd9cc5036..cd6ade912 100644 --- a/tests/integration/env.js +++ b/tests/integration/env.js @@ -6,7 +6,7 @@ const { __dirname, require } = createEsmUtils(import.meta); const isProduction = process.env.NODE_ENV === "production"; const { PRETTIER_DIR } = process.env; const { bin } = require(path.join(PRETTIER_DIR, "package.json")); -const prettierCli = path.join( +const prettierCliEntry = path.join( PRETTIER_DIR, typeof bin === "object" ? bin.prettier : bin, ); @@ -17,4 +17,14 @@ const prettierMainEntry = isProduction const projectRoot = path.join(__dirname, "../.."); -export { isProduction, prettierCli, prettierMainEntry, projectRoot }; +const prettierCliMockableEntry = isProduction + ? path.join(PRETTIER_DIR, "./internal/legacy-cli.mjs") + : path.join(PRETTIER_DIR, "./src/cli/index.js"); + +export { + isProduction, + prettierCliEntry, + prettierCliMockableEntry, + prettierMainEntry, + projectRoot, +}; diff --git a/tests/integration/run-cli.js b/tests/integration/run-cli.js index 84cb53d3d..b0547a203 100644 --- a/tests/integration/run-cli.js +++ b/tests/integration/run-cli.js @@ -26,6 +26,16 @@ const removeFinalNewLine = (string) => string.endsWith("\n") ? string.slice(0, -1) : string; const SUPPORTS_DISABLE_WARNING_FLAG = Number(process.versions.node.split(".")[0]) >= 20; +const promiseWithResolvers = Promise.withResolvers + ? Promise.withResolvers.bind(Promise) + : () => { + let resolve; + let reject; + const promise = new Promise( + (_resolve, _reject) => ([resolve, reject] = [_resolve, _reject]), + ); + return { promise, resolve, reject }; + }; /** * @param {string} dir @@ -39,6 +49,30 @@ function runCliWorker(dir, args, options) { stderr: "", write: [], }; + const { promise, resolve, reject } = promiseWithResolvers(); + const createEpipeErrorHandler = (event) => (error) => { + if (!error) { + return; + } + + // It can fail with `write EPIPE` error when running node with unsupported flags like `--experimental-strip-types` + // Let's ignore and wait for the `close` event + if ( + error.code === "EPIPE" && + error.syscall === "write" && + nodeOptions.length > 0 + ) { + if (IS_CI) { + // eslint-disable-next-line no-console + console.error( + Object.assign(error, { event, dir, args, options, worker }), + ); + } + return; + } + + reject(error); + }; const nodeOptions = options?.nodeOptions ?? []; const worker = childProcess.fork(CLI_WORKER_FILE, args, { @@ -55,11 +89,14 @@ function runCliWorker(dir, args, options) { ...process.env, NO_COLOR: "1", }, + serialization: "advanced", }); - worker.on("message", ({ action, data }) => { - if (action === "write-file") { - result.write.push(data); + worker.on("message", (message) => { + if (message.type === "cli:write-file") { + result.write.push(message.data); + } else if (message.type === "worker:fault") { + reject(message.error); } }); @@ -69,49 +106,32 @@ function runCliWorker(dir, args, options) { }); } - if (options.input) { - worker.stdin.end(options.input); - } - const removeStdioFinalNewLine = () => { for (const stream of ["stdout", "stderr"]) { result[stream] = removeFinalNewLine(result[stream]); } }; - return new Promise((resolve, reject) => { - worker.once("close", (code) => { - result.status = code; - removeStdioFinalNewLine(); - resolve(result); - }); - - worker.once("error", (error) => { - reject(error); - }); - - worker.send(options, (error) => { - if (!error) { - return; - } - - // It can fail with `write EPIPE` error when running node with unsupported flags like `--experimental-strip-types` - // Let's ignore and wait for the `close` event - if ( - error.code === "EPIPE" && - error.syscall === "write" && - nodeOptions.length > 0 - ) { - if (IS_CI) { - // eslint-disable-next-line no-console - console.error(Object.assign(error, { dir, args, options, worker })); - } - return; - } - - reject(error); - }); + worker.once("close", (code) => { + result.status = code; + removeStdioFinalNewLine(); + resolve(result); }); + + worker.once("error", (error) => { + reject(error); + }); + + worker.once("spawn", () => { + if (options.input) { + worker.stdin.once("error", createEpipeErrorHandler("worker.stdin.end()")); + worker.stdin.end(options.input); + } + + worker.send(options, createEpipeErrorHandler("worker.send()")); + }); + + return promise; } function runPrettierCli(dir, args, options) {