From 7f1dfddedd03b3c68a01dbbc895f31da0b11330c Mon Sep 17 00:00:00 2001 From: Cole Ellison Date: Wed, 22 Jul 2026 12:38:27 -0400 Subject: [PATCH 1/2] feat: perform format operation in chunks --- src/index.ts | 54 +++++++++---------- src/map_settled_with_concurrency.ts | 31 +++++++++++ .../__tests__/map-settled-with-concurrency.js | 22 ++++++++ 3 files changed, 80 insertions(+), 27 deletions(-) create mode 100644 src/map_settled_with_concurrency.ts create mode 100644 test/__tests__/map-settled-with-concurrency.js diff --git a/src/index.ts b/src/index.ts index 4d0ad2e..27ce6fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import isBinaryPath from "is-binary-path"; import stringify from "json-sorted-stringify"; +import os from "node:os"; import path from "node:path"; import process from "node:process"; import Cache from "./cache.js"; @@ -10,6 +11,7 @@ import { Loaders, File2Loader, getPrettierConfigsMap, getPrettierConfigResolved import { PRETTIER_VERSION, CLI_VERSION } from "./constants.js"; import Known from "./known.js"; import Logger from "./logger.js"; +import { mapSettledWithConcurrency } from "./map_settled_with_concurrency.js"; import { makePrettier } from "./prettier.js"; import { castArray, @@ -179,34 +181,32 @@ async function runGlobs(options: Options, pluginsDefaultOptions: PluginsOptions, const cache = shouldCache ? new Cache(cacheVersion, projectPath, getCacheRootPath(rootPath), options, stdout) : undefined; const prettier = await makePrettier(options, cache); - //TODO: Maybe do work in chunks here, as keeping too many formatted files in memory can be a problem - const filesResults = await Promise.allSettled( - filesPathsTargets.map(async (filePath) => { - const isIgnored = () => (ignoreManual ? ignoreManual(filePath) : getIgnoreResolved(filePath, ignoreNames)); - const isCacheable = () => cache?.has(filePath, isIgnored); - const isExplicitlyIncluded = () => filesExplicitPathsSet.has(filePath); - const isForceIncluded = options.dump && isExplicitlyIncluded(); - const isExcluded = cache ? !(await isCacheable()) : await isIgnored(); - if (!isForceIncluded && isExcluded) return; - const getFormatOptions = async (): Promise => { - const editorConfig = options.editorConfig ? getEditorConfigFormatOptions(await getEditorConfigResolved(filePath, editorConfigNames)) : {}; - const prettierConfig = prettierManualConfig || (options.config ? await getPrettierConfigResolved(filePath, prettierConfigNames) : {}); - const formatOptions = { ...editorConfig, ...prettierConfig, ...options.formatOptions }; - return formatOptions; - }; - try { - if (options.check || options.list) { - return await prettier.checkWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions); - } else if (options.write) { - return await prettier.writeWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions); - } else { - return await prettier.formatWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions); - } - } finally { - spinner?.update(fastRelativePath(rootPath, filePath)); + const filesConcurrency = options.parallel ? options.parallelWorkers || Math.max(1, os.cpus().length - 1) : 1; + const filesResults = await mapSettledWithConcurrency(filesPathsTargets, filesConcurrency, async (filePath) => { + const isIgnored = () => (ignoreManual ? ignoreManual(filePath) : getIgnoreResolved(filePath, ignoreNames)); + const isCacheable = () => cache?.has(filePath, isIgnored); + const isExplicitlyIncluded = () => filesExplicitPathsSet.has(filePath); + const isForceIncluded = options.dump && isExplicitlyIncluded(); + const isExcluded = cache ? !(await isCacheable()) : await isIgnored(); + if (!isForceIncluded && isExcluded) return; + const getFormatOptions = async (): Promise => { + const editorConfig = options.editorConfig ? getEditorConfigFormatOptions(await getEditorConfigResolved(filePath, editorConfigNames)) : {}; + const prettierConfig = prettierManualConfig || (options.config ? await getPrettierConfigResolved(filePath, prettierConfigNames) : {}); + const formatOptions = { ...editorConfig, ...prettierConfig, ...options.formatOptions }; + return formatOptions; + }; + try { + if (options.check || options.list) { + return await prettier.checkWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions); + } else if (options.write) { + return await prettier.writeWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions); + } else { + return await prettier.formatWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions); } - }), - ); + } finally { + spinner?.update(fastRelativePath(rootPath, filePath)); + } + }); spinner?.stop("Checking formatting..."); diff --git a/src/map_settled_with_concurrency.ts b/src/map_settled_with_concurrency.ts new file mode 100644 index 0000000..f933bdc --- /dev/null +++ b/src/map_settled_with_concurrency.ts @@ -0,0 +1,31 @@ +async function mapSettledWithConcurrency( + values: readonly T[], + concurrency: number, + mapper: (value: T, index: number) => Promise, +): Promise[]> { + const results = new Array>(values.length); + let nextIndex = 0; + + async function runNext(): Promise { + while (true) { + const index = nextIndex++; + if (index >= values.length) return; + + try { + results[index] = { + status: "fulfilled", + value: await mapper(values[index], index), + }; + } catch (reason) { + results[index] = { status: "rejected", reason }; + } + } + } + + const runnersCount = Math.min(values.length, Math.max(1, Math.floor(concurrency))); + await Promise.all(Array.from({ length: runnersCount }, runNext)); + + return results; +} + +export { mapSettledWithConcurrency }; diff --git a/test/__tests__/map-settled-with-concurrency.js b/test/__tests__/map-settled-with-concurrency.js new file mode 100644 index 0000000..18f6696 --- /dev/null +++ b/test/__tests__/map-settled-with-concurrency.js @@ -0,0 +1,22 @@ +import { expect, test } from "@jest/globals"; +import { mapSettledWithConcurrency } from "../../dist/map_settled_with_concurrency.js"; + +test("bounds concurrency and preserves settled-result order", async () => { + let active = 0; + let maxActive = 0; + + const results = await mapSettledWithConcurrency([0, 1, 2, 3, 4, 5], 3, async (value) => { + active += 1; + maxActive = Math.max(maxActive, active); + await new Promise((resolve) => setImmediate(resolve)); + active -= 1; + if (value === 2) throw new Error("expected failure"); + return value * 2; + }); + + expect(maxActive).toBe(3); + expect(results.map((result) => result.status)).toEqual(["fulfilled", "fulfilled", "rejected", "fulfilled", "fulfilled", "fulfilled"]); + expect(results[0]).toEqual({ status: "fulfilled", value: 0 }); + expect(results[2]).toMatchObject({ status: "rejected", reason: new Error("expected failure") }); + expect(results[5]).toEqual({ status: "fulfilled", value: 10 }); +}); From 1e711f9df89bbb3e240086b8cf4624be632c97bc Mon Sep 17 00:00:00 2001 From: Cole Ellison Date: Thu, 30 Jul 2026 16:58:32 -0400 Subject: [PATCH 2/2] move mapSettledWithConcurrency to utils.ts --- src/index.ts | 2 +- src/map_settled_with_concurrency.ts | 31 ------------------- src/utils.ts | 31 +++++++++++++++++++ .../__tests__/map-settled-with-concurrency.js | 2 +- 4 files changed, 33 insertions(+), 33 deletions(-) delete mode 100644 src/map_settled_with_concurrency.ts diff --git a/src/index.ts b/src/index.ts index 27ce6fa..52a235b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,6 @@ import { Loaders, File2Loader, getPrettierConfigsMap, getPrettierConfigResolved import { PRETTIER_VERSION, CLI_VERSION } from "./constants.js"; import Known from "./known.js"; import Logger from "./logger.js"; -import { mapSettledWithConcurrency } from "./map_settled_with_concurrency.js"; import { makePrettier } from "./prettier.js"; import { castArray, @@ -23,6 +22,7 @@ import { getProjectPath, getStdin, getTargetsPaths, + mapSettledWithConcurrency, } from "./utils.js"; import { fastRelativePath, diff --git a/src/map_settled_with_concurrency.ts b/src/map_settled_with_concurrency.ts deleted file mode 100644 index f933bdc..0000000 --- a/src/map_settled_with_concurrency.ts +++ /dev/null @@ -1,31 +0,0 @@ -async function mapSettledWithConcurrency( - values: readonly T[], - concurrency: number, - mapper: (value: T, index: number) => Promise, -): Promise[]> { - const results = new Array>(values.length); - let nextIndex = 0; - - async function runNext(): Promise { - while (true) { - const index = nextIndex++; - if (index >= values.length) return; - - try { - results[index] = { - status: "fulfilled", - value: await mapper(values[index], index), - }; - } catch (reason) { - results[index] = { status: "rejected", reason }; - } - } - } - - const runnersCount = Math.min(values.length, Math.max(1, Math.floor(concurrency))); - await Promise.all(Array.from({ length: runnersCount }, runNext)); - - return results; -} - -export { mapSettledWithConcurrency }; diff --git a/src/utils.ts b/src/utils.ts index 65fcd63..be828f1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -635,6 +635,36 @@ function zipObjectUnless(keys: T[], values: U[], unless: (valu return map; } +async function mapSettledWithConcurrency( + values: readonly T[], + concurrency: number, + mapper: (value: T, index: number) => Promise, +): Promise[]> { + const results = new Array>(values.length); + let nextIndex = 0; + + async function runNext(): Promise { + while (true) { + const index = nextIndex++; + if (index >= values.length) return; + + try { + results[index] = { + status: "fulfilled", + value: await mapper(values[index], index), + }; + } catch (reason) { + results[index] = { status: "rejected", reason }; + } + } + } + + const runnersCount = Math.min(values.length, Math.max(1, Math.floor(concurrency))); + await Promise.all(Array.from({ length: runnersCount }, runNext)); + + return results; +} + export { castArray, fastJoinedPath, @@ -671,6 +701,7 @@ export { isString, isTruthy, isUndefined, + mapSettledWithConcurrency, memoize, negate, noop, diff --git a/test/__tests__/map-settled-with-concurrency.js b/test/__tests__/map-settled-with-concurrency.js index 18f6696..0ee0835 100644 --- a/test/__tests__/map-settled-with-concurrency.js +++ b/test/__tests__/map-settled-with-concurrency.js @@ -1,5 +1,5 @@ import { expect, test } from "@jest/globals"; -import { mapSettledWithConcurrency } from "../../dist/map_settled_with_concurrency.js"; +import { mapSettledWithConcurrency } from "../../dist/utils.js"; test("bounds concurrency and preserves settled-result order", async () => { let active = 0;