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 }); +});