From 660611ca1c3ee9d85e172bebaf2bd81259e36c53 Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 2 Aug 2026 10:01:05 +0800 Subject: [PATCH 1/4] feat(fmt): support custom parallel worker count --- packages/rstack/src/fmt/cli.ts | 38 +++++++++++++- packages/rstack/src/fmt/parallel.ts | 10 ++-- packages/rstack/src/fmt/runner.ts | 6 ++- packages/rstack/src/fmt/types.ts | 2 + packages/rstack/tests/cli/fmt/index.test.ts | 7 ++- packages/rstack/tests/fmt/cli.test.ts | 52 ++++++++++++++++--- packages/rstack/tests/fmt/parallel.test.ts | 17 ++++++ .../tests/fmt/runnerParallelPreflight.test.ts | 18 ++++++- 8 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 packages/rstack/tests/fmt/parallel.test.ts diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 602e4bc..6df717d 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -11,6 +11,7 @@ interface ParsedFmtCLIArgs { mode: FmtMode; patterns: string[]; parallel: boolean; + parallelWorkers?: number; help: boolean; } @@ -26,8 +27,22 @@ ${color.cyan('Options')}: --check Check whether files are formatted --list-different Print paths of unformatted files --no-parallel Disable worker parallelism + --parallel-workers Number of parallel workers -h, --help Display this help message`; +const parseParallelWorkers = (value: string | undefined): number | undefined => { + if (value === undefined) { + return undefined; + } + + const parallelWorkers = Number(value); + if (!/^\d+$/.test(value) || !Number.isSafeInteger(parallelWorkers) || parallelWorkers < 1) { + throw new Error('The --parallel-workers option must be a positive integer.'); + } + + return parallelWorkers; +}; + const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { const { values, positionals } = parseArgs({ args, @@ -38,6 +53,8 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { listDifferent: { type: 'boolean' }, 'no-parallel': { type: 'boolean' }, noParallel: { type: 'boolean' }, + 'parallel-workers': { type: 'string' }, + parallelWorkers: { type: 'string' }, help: { type: 'boolean', short: 'h' }, }, allowPositionals: true, @@ -51,11 +68,27 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { } const mode = values.check ? 'check' : listDifferent ? 'list-different' : 'write'; + const noParallel = values['no-parallel'] || values.noParallel; + const kebabParallelWorkers = values['parallel-workers']; + const camelParallelWorkers = values.parallelWorkers; + + if (kebabParallelWorkers !== undefined && camelParallelWorkers !== undefined) { + throw new Error( + 'The --parallel-workers and --parallelWorkers options cannot be used together.', + ); + } + + const parallelWorkers = parseParallelWorkers(kebabParallelWorkers ?? camelParallelWorkers); + + if (noParallel && parallelWorkers !== undefined) { + throw new Error('The --parallel-workers and --no-parallel options cannot be used together.'); + } return { mode, patterns: positionals, - parallel: !(values['no-parallel'] || values.noParallel), + parallel: !noParallel, + parallelWorkers, help: values.help ?? false, }; }; @@ -98,7 +131,7 @@ const logFmtResult = (result: FmtRunResult, mode: FmtMode, cwd: string): void => }; const runFmtCLI = async (args: string[]): Promise => { - const { help, mode, parallel, patterns } = parseFmtCLIArgs(args); + const { help, mode, parallel, parallelWorkers, patterns } = parseFmtCLIArgs(args); if (help) { console.log(fmtHelpMessage); return; @@ -124,6 +157,7 @@ const runFmtCLI = async (args: string[]): Promise => { mode, cache: false, parallel, + parallelWorkers, }); logFmtResult(result, mode, cwd); diff --git a/packages/rstack/src/fmt/parallel.ts b/packages/rstack/src/fmt/parallel.ts index 8907a05..6825784 100644 --- a/packages/rstack/src/fmt/parallel.ts +++ b/packages/rstack/src/fmt/parallel.ts @@ -10,8 +10,8 @@ interface FmtWorker { terminate: () => void; } -const getFmtWorkerCount = (fileCount: number): number => - Math.min(fileCount, Math.max(1, availableParallelism() - 1)); +const getFmtWorkerCount = (fileCount: number, parallelWorkers?: number): number => + Math.min(fileCount, parallelWorkers ?? Math.max(1, availableParallelism() - 1)); const getFmtWorkerUrl = (): URL => { // Source tests run after build and exercise the same worker artifact as the CLI. @@ -22,8 +22,8 @@ const getFmtWorkerUrl = (): URL => { }; /** Creates and starts every worker before formatting can begin. */ -const createFmtWorker = async (fileCount: number): Promise => { - const workerCount = getFmtWorkerCount(fileCount); +const createFmtWorker = async (fileCount: number, parallelWorkers?: number): Promise => { + const workerCount = getFmtWorkerCount(fileCount, parallelWorkers); const pool = new WorkTank({ pool: { name: 'rstack-fmt', @@ -51,4 +51,4 @@ const createFmtWorker = async (fileCount: number): Promise => { }; }; -export { createFmtWorker }; +export { createFmtWorker, getFmtWorkerCount }; diff --git a/packages/rstack/src/fmt/runner.ts b/packages/rstack/src/fmt/runner.ts index 455a006..ff5384a 100644 --- a/packages/rstack/src/fmt/runner.ts +++ b/packages/rstack/src/fmt/runner.ts @@ -54,9 +54,10 @@ const runFmtFilesSerial = async ( const runFmtFilesParallel = async ( files: FmtFileRequest[], shouldWrite: boolean, + parallelWorkers?: number, ): Promise => { const { createFmtWorker } = await import('./parallel.ts'); - const worker = await createFmtWorker(files.length); + const worker = await createFmtWorker(files.length, parallelWorkers); try { return await Promise.all(files.map((file) => runFmtFile(file, shouldWrite, worker.formatFile))); @@ -96,12 +97,13 @@ const runFmtFiles = async ({ files, mode, parallel, + parallelWorkers, }: RunFmtFilesOptions): Promise => { const startTime = performance.now(); const shouldWrite = mode === 'write'; const results = parallel && files.length > 1 && canRunFmtFilesParallel(files) - ? await runFmtFilesParallel(files, shouldWrite) + ? await runFmtFilesParallel(files, shouldWrite, parallelWorkers) : await runFmtFilesSerial(files, shouldWrite); return { diff --git a/packages/rstack/src/fmt/types.ts b/packages/rstack/src/fmt/types.ts index fba68df..151a3d1 100644 --- a/packages/rstack/src/fmt/types.ts +++ b/packages/rstack/src/fmt/types.ts @@ -56,6 +56,8 @@ interface RunFmtFilesOptions { cache: false; /** Whether cloneable file requests should run in worker threads. */ parallel: boolean; + /** Maximum worker count when parallel execution is enabled. */ + parallelWorkers?: number; } interface SuccessfulFmtFileResult { diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index 44d822a..038b7ba 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -84,11 +84,14 @@ test('formats the current directory with Prettier defaults', () => { expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); }); -test('supports disabling parallel execution', () => { +test.each([ + ['disabling parallel execution', ['--no-parallel']], + ['configuring parallel worker count', ['--parallel-workers', '1']], +] as const)('supports %s', (_, options) => { writeProjectFile('first.ts', 'const first="first"'); writeProjectFile('second.ts', 'const second="second"'); - const result = runFmt(['--no-parallel', 'first.ts', 'second.ts']); + const result = runFmt([...options, 'first.ts', 'second.ts']); expect(result.status).toBe(0); expect(result.stdout).toBe('first.ts\nsecond.ts\n'); diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index dd57260..3e1fcfe 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -6,6 +6,7 @@ test('uses write mode by default', () => { mode: 'write', patterns: [], parallel: true, + parallelWorkers: undefined, help: false, }); }); @@ -20,6 +21,7 @@ test.each([ mode, patterns: [], parallel: true, + parallelWorkers: undefined, help: false, }); }); @@ -29,10 +31,48 @@ test.each(['--no-parallel', '--noParallel'])('disables parallel execution with % mode: 'write', patterns: [], parallel: false, + parallelWorkers: undefined, help: false, }); }); +test.each(['--parallel-workers', '--parallelWorkers'])( + 'configures parallel worker count with %s', + (option) => { + expect(parseFmtCLIArgs([option, '3'])).toEqual({ + mode: 'write', + patterns: [], + parallel: true, + parallelWorkers: 3, + help: false, + }); + }, +); + +test.each(['0', '-1', '1.5', 'invalid', '9007199254740992'])( + 'rejects invalid parallel worker count %s', + (count) => { + expect(() => parseFmtCLIArgs([`--parallel-workers=${count}`])).toThrow( + 'The --parallel-workers option must be a positive integer.', + ); + }, +); + +test('rejects using both parallel worker aliases', () => { + expect(() => parseFmtCLIArgs(['--parallel-workers', '2', '--parallelWorkers', '3'])).toThrow( + 'The --parallel-workers and --parallelWorkers options cannot be used together.', + ); +}); + +test.each([ + ['--no-parallel', '--parallel-workers'], + ['--noParallel', '--parallelWorkers'], +])('rejects conflicting parallel options: %s and %s', (noParallel, parallelWorkers) => { + expect(() => parseFmtCLIArgs([noParallel, parallelWorkers, '2'])).toThrow( + 'The --parallel-workers and --no-parallel options cannot be used together.', + ); +}); + test('preserves file paths and globs', () => { const patterns = ['src/file with spaces.ts', 'src/**/*.{js,ts}', '!src/generated/**']; @@ -40,6 +80,7 @@ test('preserves file paths and globs', () => { mode: 'check', patterns, parallel: true, + parallelWorkers: undefined, help: false, }); }); @@ -49,6 +90,7 @@ test('treats arguments after the terminator as paths', () => { mode: 'check', patterns: ['--write', '--help'], parallel: true, + parallelWorkers: undefined, help: false, }); }); @@ -63,6 +105,7 @@ test('provides command help', () => { expect(fmtHelpMessage).toContain('--check'); expect(fmtHelpMessage).toContain('--list-different'); expect(fmtHelpMessage).toContain('--no-parallel'); + expect(fmtHelpMessage).toContain('--parallel-workers '); expect(fmtHelpMessage).toContain('-h, --help'); }); @@ -78,9 +121,6 @@ test.each([ ); }); -test.each(['--unknown', '--no-cache', '--parallel-workers'])( - 'rejects unsupported option %s', - (option) => { - expect(() => parseFmtCLIArgs([option])).toThrow(); - }, -); +test.each(['--unknown', '--no-cache'])('rejects unsupported option %s', (option) => { + expect(() => parseFmtCLIArgs([option])).toThrow(); +}); diff --git a/packages/rstack/tests/fmt/parallel.test.ts b/packages/rstack/tests/fmt/parallel.test.ts new file mode 100644 index 0000000..977bbf4 --- /dev/null +++ b/packages/rstack/tests/fmt/parallel.test.ts @@ -0,0 +1,17 @@ +import { availableParallelism } from 'node:os'; +import { expect, test } from 'rstack/test'; +import { getFmtWorkerCount } from '../../src/fmt/parallel.ts'; + +test('uses one fewer worker than the available parallelism by default', () => { + const defaultWorkerCount = Math.max(1, availableParallelism() - 1); + + expect(getFmtWorkerCount(defaultWorkerCount + 1)).toBe(defaultWorkerCount); +}); + +test.each([ + [4, 1, 1], + [4, 2, 2], + [2, 4, 2], +])('uses %s files and %s configured workers as %s workers', (files, workers, expected) => { + expect(getFmtWorkerCount(files, workers)).toBe(expected); +}); diff --git a/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts b/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts index 061083e..9e20cd5 100644 --- a/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts @@ -1,13 +1,24 @@ import { readFileSync } from 'node:fs'; -import { expect, rs, test } from 'rstack/test'; +import { beforeEach, expect, rs, test } from 'rstack/test'; import { runFmtFiles } from '../../src/fmt/runner.ts'; import type { FmtFileRequest } from '../../src/fmt/types.ts'; import { withTempProject, writeProjectFile } from './helpers.ts'; +const mocks = rs.hoisted(() => ({ + createFmtWorkerCalls: [] as [number, number | undefined][], +})); + rs.mock('../../src/fmt/parallel.ts', () => ({ - createFmtWorker: () => Promise.reject(new Error('worker startup failed')), + createFmtWorker: (fileCount: number, parallelWorkers?: number) => { + mocks.createFmtWorkerCalls.push([fileCount, parallelWorkers]); + return Promise.reject(new Error('worker startup failed')); + }, })); +beforeEach(() => { + mocks.createFmtWorkerCalls.length = 0; +}); + const createRequest = ( filePath: string, plugins?: FmtFileRequest['options']['plugins'], @@ -32,9 +43,12 @@ test('does not write files when worker startup fails', async () => { mode: 'write', cache: false, parallel: true, + parallelWorkers: 3, }), ).rejects.toThrow('worker startup failed'); + expect(mocks.createFmtWorkerCalls).toEqual([[2, 3]]); + for (const filePath of filePaths) { expect(readFileSync(filePath, 'utf8')).toBe('const value=1'); } From 870396b6d3634f928e0fae3aa88e0be8378a6448 Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 2 Aug 2026 10:06:29 +0800 Subject: [PATCH 2/4] refactor(fmt): clarify worker count naming --- packages/rstack/src/fmt/cli.ts | 26 +++++++++---------- packages/rstack/src/fmt/parallel.ts | 8 +++--- packages/rstack/src/fmt/runner.ts | 8 +++--- packages/rstack/src/fmt/types.ts | 2 +- packages/rstack/tests/fmt/cli.test.ts | 16 ++++++------ .../tests/fmt/runnerParallelPreflight.test.ts | 6 ++--- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 6df717d..c678a29 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -11,7 +11,7 @@ interface ParsedFmtCLIArgs { mode: FmtMode; patterns: string[]; parallel: boolean; - parallelWorkers?: number; + maxWorkers?: number; help: boolean; } @@ -30,17 +30,17 @@ ${color.cyan('Options')}: --parallel-workers Number of parallel workers -h, --help Display this help message`; -const parseParallelWorkers = (value: string | undefined): number | undefined => { +const parseMaxWorkers = (value: string | undefined): number | undefined => { if (value === undefined) { return undefined; } - const parallelWorkers = Number(value); - if (!/^\d+$/.test(value) || !Number.isSafeInteger(parallelWorkers) || parallelWorkers < 1) { + const maxWorkers = Number(value); + if (!/^\d+$/.test(value) || !Number.isSafeInteger(maxWorkers) || maxWorkers < 1) { throw new Error('The --parallel-workers option must be a positive integer.'); } - return parallelWorkers; + return maxWorkers; }; const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { @@ -69,18 +69,18 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { const mode = values.check ? 'check' : listDifferent ? 'list-different' : 'write'; const noParallel = values['no-parallel'] || values.noParallel; - const kebabParallelWorkers = values['parallel-workers']; - const camelParallelWorkers = values.parallelWorkers; + const kebabMaxWorkers = values['parallel-workers']; + const camelMaxWorkers = values.parallelWorkers; - if (kebabParallelWorkers !== undefined && camelParallelWorkers !== undefined) { + if (kebabMaxWorkers !== undefined && camelMaxWorkers !== undefined) { throw new Error( 'The --parallel-workers and --parallelWorkers options cannot be used together.', ); } - const parallelWorkers = parseParallelWorkers(kebabParallelWorkers ?? camelParallelWorkers); + const maxWorkers = parseMaxWorkers(kebabMaxWorkers ?? camelMaxWorkers); - if (noParallel && parallelWorkers !== undefined) { + if (noParallel && maxWorkers !== undefined) { throw new Error('The --parallel-workers and --no-parallel options cannot be used together.'); } @@ -88,7 +88,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { mode, patterns: positionals, parallel: !noParallel, - parallelWorkers, + maxWorkers, help: values.help ?? false, }; }; @@ -131,7 +131,7 @@ const logFmtResult = (result: FmtRunResult, mode: FmtMode, cwd: string): void => }; const runFmtCLI = async (args: string[]): Promise => { - const { help, mode, parallel, parallelWorkers, patterns } = parseFmtCLIArgs(args); + const { help, maxWorkers, mode, parallel, patterns } = parseFmtCLIArgs(args); if (help) { console.log(fmtHelpMessage); return; @@ -157,7 +157,7 @@ const runFmtCLI = async (args: string[]): Promise => { mode, cache: false, parallel, - parallelWorkers, + maxWorkers, }); logFmtResult(result, mode, cwd); diff --git a/packages/rstack/src/fmt/parallel.ts b/packages/rstack/src/fmt/parallel.ts index 6825784..b15ae9a 100644 --- a/packages/rstack/src/fmt/parallel.ts +++ b/packages/rstack/src/fmt/parallel.ts @@ -10,8 +10,8 @@ interface FmtWorker { terminate: () => void; } -const getFmtWorkerCount = (fileCount: number, parallelWorkers?: number): number => - Math.min(fileCount, parallelWorkers ?? Math.max(1, availableParallelism() - 1)); +const getFmtWorkerCount = (fileCount: number, maxWorkers?: number): number => + Math.min(fileCount, maxWorkers ?? Math.max(1, availableParallelism() - 1)); const getFmtWorkerUrl = (): URL => { // Source tests run after build and exercise the same worker artifact as the CLI. @@ -22,8 +22,8 @@ const getFmtWorkerUrl = (): URL => { }; /** Creates and starts every worker before formatting can begin. */ -const createFmtWorker = async (fileCount: number, parallelWorkers?: number): Promise => { - const workerCount = getFmtWorkerCount(fileCount, parallelWorkers); +const createFmtWorker = async (fileCount: number, maxWorkers?: number): Promise => { + const workerCount = getFmtWorkerCount(fileCount, maxWorkers); const pool = new WorkTank({ pool: { name: 'rstack-fmt', diff --git a/packages/rstack/src/fmt/runner.ts b/packages/rstack/src/fmt/runner.ts index ff5384a..439be64 100644 --- a/packages/rstack/src/fmt/runner.ts +++ b/packages/rstack/src/fmt/runner.ts @@ -54,10 +54,10 @@ const runFmtFilesSerial = async ( const runFmtFilesParallel = async ( files: FmtFileRequest[], shouldWrite: boolean, - parallelWorkers?: number, + maxWorkers?: number, ): Promise => { const { createFmtWorker } = await import('./parallel.ts'); - const worker = await createFmtWorker(files.length, parallelWorkers); + const worker = await createFmtWorker(files.length, maxWorkers); try { return await Promise.all(files.map((file) => runFmtFile(file, shouldWrite, worker.formatFile))); @@ -97,13 +97,13 @@ const runFmtFiles = async ({ files, mode, parallel, - parallelWorkers, + maxWorkers, }: RunFmtFilesOptions): Promise => { const startTime = performance.now(); const shouldWrite = mode === 'write'; const results = parallel && files.length > 1 && canRunFmtFilesParallel(files) - ? await runFmtFilesParallel(files, shouldWrite, parallelWorkers) + ? await runFmtFilesParallel(files, shouldWrite, maxWorkers) : await runFmtFilesSerial(files, shouldWrite); return { diff --git a/packages/rstack/src/fmt/types.ts b/packages/rstack/src/fmt/types.ts index 151a3d1..8254ce0 100644 --- a/packages/rstack/src/fmt/types.ts +++ b/packages/rstack/src/fmt/types.ts @@ -57,7 +57,7 @@ interface RunFmtFilesOptions { /** Whether cloneable file requests should run in worker threads. */ parallel: boolean; /** Maximum worker count when parallel execution is enabled. */ - parallelWorkers?: number; + maxWorkers?: number; } interface SuccessfulFmtFileResult { diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index 3e1fcfe..3624986 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -6,7 +6,7 @@ test('uses write mode by default', () => { mode: 'write', patterns: [], parallel: true, - parallelWorkers: undefined, + maxWorkers: undefined, help: false, }); }); @@ -21,7 +21,7 @@ test.each([ mode, patterns: [], parallel: true, - parallelWorkers: undefined, + maxWorkers: undefined, help: false, }); }); @@ -31,7 +31,7 @@ test.each(['--no-parallel', '--noParallel'])('disables parallel execution with % mode: 'write', patterns: [], parallel: false, - parallelWorkers: undefined, + maxWorkers: undefined, help: false, }); }); @@ -43,7 +43,7 @@ test.each(['--parallel-workers', '--parallelWorkers'])( mode: 'write', patterns: [], parallel: true, - parallelWorkers: 3, + maxWorkers: 3, help: false, }); }, @@ -67,8 +67,8 @@ test('rejects using both parallel worker aliases', () => { test.each([ ['--no-parallel', '--parallel-workers'], ['--noParallel', '--parallelWorkers'], -])('rejects conflicting parallel options: %s and %s', (noParallel, parallelWorkers) => { - expect(() => parseFmtCLIArgs([noParallel, parallelWorkers, '2'])).toThrow( +])('rejects conflicting parallel options: %s and %s', (noParallel, maxWorkersOption) => { + expect(() => parseFmtCLIArgs([noParallel, maxWorkersOption, '2'])).toThrow( 'The --parallel-workers and --no-parallel options cannot be used together.', ); }); @@ -80,7 +80,7 @@ test('preserves file paths and globs', () => { mode: 'check', patterns, parallel: true, - parallelWorkers: undefined, + maxWorkers: undefined, help: false, }); }); @@ -90,7 +90,7 @@ test('treats arguments after the terminator as paths', () => { mode: 'check', patterns: ['--write', '--help'], parallel: true, - parallelWorkers: undefined, + maxWorkers: undefined, help: false, }); }); diff --git a/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts b/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts index 9e20cd5..415c336 100644 --- a/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts @@ -9,8 +9,8 @@ const mocks = rs.hoisted(() => ({ })); rs.mock('../../src/fmt/parallel.ts', () => ({ - createFmtWorker: (fileCount: number, parallelWorkers?: number) => { - mocks.createFmtWorkerCalls.push([fileCount, parallelWorkers]); + createFmtWorker: (fileCount: number, maxWorkers?: number) => { + mocks.createFmtWorkerCalls.push([fileCount, maxWorkers]); return Promise.reject(new Error('worker startup failed')); }, })); @@ -43,7 +43,7 @@ test('does not write files when worker startup fails', async () => { mode: 'write', cache: false, parallel: true, - parallelWorkers: 3, + maxWorkers: 3, }), ).rejects.toThrow('worker startup failed'); From 11fdf66419e0d09a538ffed53bfaf2dbd5e31ded Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 2 Aug 2026 10:09:00 +0800 Subject: [PATCH 3/4] refactor(fmt): centralize worker option parsing --- packages/rstack/src/fmt/cli.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index c678a29..f90956e 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -30,7 +30,17 @@ ${color.cyan('Options')}: --parallel-workers Number of parallel workers -h, --help Display this help message`; -const parseMaxWorkers = (value: string | undefined): number | undefined => { +const parseMaxWorkers = ( + kebabValue: string | undefined, + camelValue: string | undefined, +): number | undefined => { + if (kebabValue !== undefined && camelValue !== undefined) { + throw new Error( + 'The --parallel-workers and --parallelWorkers options cannot be used together.', + ); + } + + const value = kebabValue ?? camelValue; if (value === undefined) { return undefined; } @@ -69,16 +79,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { const mode = values.check ? 'check' : listDifferent ? 'list-different' : 'write'; const noParallel = values['no-parallel'] || values.noParallel; - const kebabMaxWorkers = values['parallel-workers']; - const camelMaxWorkers = values.parallelWorkers; - - if (kebabMaxWorkers !== undefined && camelMaxWorkers !== undefined) { - throw new Error( - 'The --parallel-workers and --parallelWorkers options cannot be used together.', - ); - } - - const maxWorkers = parseMaxWorkers(kebabMaxWorkers ?? camelMaxWorkers); + const maxWorkers = parseMaxWorkers(values['parallel-workers'], values.parallelWorkers); if (noParallel && maxWorkers !== undefined) { throw new Error('The --parallel-workers and --no-parallel options cannot be used together.'); From 3f5e4c3dc5d3b9a296c4c643298971e86a75d002 Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 2 Aug 2026 10:11:16 +0800 Subject: [PATCH 4/4] refactor(fmt): prefer kebab worker option --- packages/rstack/src/fmt/cli.ts | 6 ------ packages/rstack/tests/fmt/cli.test.ts | 6 ++---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index f90956e..86514b1 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -34,12 +34,6 @@ const parseMaxWorkers = ( kebabValue: string | undefined, camelValue: string | undefined, ): number | undefined => { - if (kebabValue !== undefined && camelValue !== undefined) { - throw new Error( - 'The --parallel-workers and --parallelWorkers options cannot be used together.', - ); - } - const value = kebabValue ?? camelValue; if (value === undefined) { return undefined; diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index 3624986..8eb4519 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -58,10 +58,8 @@ test.each(['0', '-1', '1.5', 'invalid', '9007199254740992'])( }, ); -test('rejects using both parallel worker aliases', () => { - expect(() => parseFmtCLIArgs(['--parallel-workers', '2', '--parallelWorkers', '3'])).toThrow( - 'The --parallel-workers and --parallelWorkers options cannot be used together.', - ); +test('prefers the kebab-case parallel worker option', () => { + expect(parseFmtCLIArgs(['--parallel-workers', '2', '--parallelWorkers', '3']).maxWorkers).toBe(2); }); test.each([