From 6bb7addffc667a581af2b2d14e4c09afd7761aea Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 1 Aug 2026 17:43:02 +0800 Subject: [PATCH 1/2] feat(fmt): enable parallel formatting --- packages/rstack/THIRD_PARTY_NOTICES.md | 143 ++++++++++++++++++ packages/rstack/package.json | 3 +- packages/rstack/src/fmt/cli.ts | 9 +- packages/rstack/src/fmt/parallel.ts | 58 +++++++ packages/rstack/src/fmt/runner.ts | 37 ++++- packages/rstack/src/fmt/types.ts | 4 +- packages/rstack/src/fmt/worker.ts | 13 +- packages/rstack/tests/cli/fmt/index.test.ts | 13 ++ packages/rstack/tests/fmt/cli.test.ts | 16 +- packages/rstack/tests/fmt/runner.test.ts | 40 ++++- .../tests/fmt/runnerParallelPreflight.test.ts | 66 ++++++++ pnpm-lock.yaml | 38 +++++ pnpm-workspace.yaml | 1 + 13 files changed, 423 insertions(+), 18 deletions(-) create mode 100644 packages/rstack/src/fmt/parallel.ts create mode 100644 packages/rstack/tests/fmt/runnerParallelPreflight.test.ts diff --git a/packages/rstack/THIRD_PARTY_NOTICES.md b/packages/rstack/THIRD_PARTY_NOTICES.md index 362f5c9..f9b89c2 100644 --- a/packages/rstack/THIRD_PARTY_NOTICES.md +++ b/packages/rstack/THIRD_PARTY_NOTICES.md @@ -171,6 +171,63 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## isoconcurrency + +This package includes bundled code from +[isoconcurrency](https://github.com/fabiospampinato/isoconcurrency). + +License: MIT + +The MIT License (MIT) + +Copyright (c) 2025-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +## isotimer + +This package includes bundled code from [isotimer](https://github.com/fabiospampinato/isotimer). + +License: MIT + +The MIT License (MIT) + +Copyright (c) 2025-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + ## micromatch This package includes bundled code from [micromatch](https://github.com/micromatch/micromatch). @@ -283,6 +340,35 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## promise-make-naked + +This package includes bundled code from +[promise-make-naked](https://github.com/fabiospampinato/promise-make-naked). + +License: MIT + +The MIT License (MIT) + +Copyright (c) 2021-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + ## tinyexec This package includes bundled code from [tinyexec](https://github.com/tinylibs/tinyexec). @@ -338,3 +424,60 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## webworker-shim + +This package includes bundled code from +[webworker-shim](https://github.com/fabiospampinato/webworker-shim). + +License: MIT + +The MIT License (MIT) + +Copyright (c) 2022-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +## worktank + +This package includes bundled code from [worktank](https://github.com/fabiospampinato/worktank). + +License: MIT + +The MIT License (MIT) + +Copyright (c) 2021-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/packages/rstack/package.json b/packages/rstack/package.json index b037059..6341aff 100644 --- a/packages/rstack/package.json +++ b/packages/rstack/package.json @@ -75,7 +75,8 @@ "micromatch": "catalog:", "rslog": "catalog:", "tiny-readdir": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "worktank": "catalog:" }, "peerDependencies": { "@rspress/core": "^2.0.17" diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index a0ab669..602e4bc 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -10,6 +10,7 @@ import type { FmtMode, FmtRunResult } from './types.ts'; interface ParsedFmtCLIArgs { mode: FmtMode; patterns: string[]; + parallel: boolean; help: boolean; } @@ -24,6 +25,7 @@ ${color.cyan('Options')}: --write Write formatted files in place (default) --check Check whether files are formatted --list-different Print paths of unformatted files + --no-parallel Disable worker parallelism -h, --help Display this help message`; const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { @@ -34,6 +36,8 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { check: { type: 'boolean' }, 'list-different': { type: 'boolean' }, listDifferent: { type: 'boolean' }, + 'no-parallel': { type: 'boolean' }, + noParallel: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, }, allowPositionals: true, @@ -51,6 +55,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { return { mode, patterns: positionals, + parallel: !(values['no-parallel'] || values.noParallel), help: values.help ?? false, }; }; @@ -93,7 +98,7 @@ const logFmtResult = (result: FmtRunResult, mode: FmtMode, cwd: string): void => }; const runFmtCLI = async (args: string[]): Promise => { - const { help, mode, patterns } = parseFmtCLIArgs(args); + const { help, mode, parallel, patterns } = parseFmtCLIArgs(args); if (help) { console.log(fmtHelpMessage); return; @@ -118,7 +123,7 @@ const runFmtCLI = async (args: string[]): Promise => { files, mode, cache: false, - parallel: false, + parallel, }); logFmtResult(result, mode, cwd); diff --git a/packages/rstack/src/fmt/parallel.ts b/packages/rstack/src/fmt/parallel.ts new file mode 100644 index 0000000..4223df7 --- /dev/null +++ b/packages/rstack/src/fmt/parallel.ts @@ -0,0 +1,58 @@ +/** + * Derived from @prettier/cli v0.12.0. + * SPDX-License-Identifier: MIT + * Modified by Rstack contributors. + */ + +import { availableParallelism } from 'node:os'; +import WorkTank from 'worktank'; + +type FmtWorkerMethods = typeof import('./worker.ts'); + +interface FmtWorker { + formatFile: FmtWorkerMethods['formatFileSerial']; + terminate: () => void; +} + +const getFmtWorkerCount = (fileCount: number): number => + Math.min(fileCount, Math.max(1, availableParallelism() - 1)); + +const getFmtWorkerUrl = (): URL => { + // Source tests run after build and exercise the same worker artifact as the CLI. + const workerPath = new URL(import.meta.url).pathname.endsWith('.ts') + ? '../../dist/fmtWorker.js' + : './fmtWorker.js'; + return new URL(workerPath, import.meta.url); +}; + +/** Creates and starts every worker before formatting can begin. */ +const createFmtWorker = async (fileCount: number): Promise => { + const workerCount = getFmtWorkerCount(fileCount); + const pool = new WorkTank({ + pool: { + name: 'rstack-fmt', + size: workerCount, + }, + worker: { + autoInstantiate: true, + methods: getFmtWorkerUrl(), + }, + }); + + try { + // Concurrent handshakes make WorkTank assign one task to every worker. + await Promise.all( + Array.from({ length: workerCount }, () => pool.exec('initializeFmtWorker', [])), + ); + } catch (error) { + pool.terminate(); + throw error; + } + + return { + formatFile: (file, shouldWrite) => pool.exec('formatFileSerial', [file, shouldWrite]), + terminate: pool.terminate, + }; +}; + +export { createFmtWorker }; diff --git a/packages/rstack/src/fmt/runner.ts b/packages/rstack/src/fmt/runner.ts index d880223..455a006 100644 --- a/packages/rstack/src/fmt/runner.ts +++ b/packages/rstack/src/fmt/runner.ts @@ -50,6 +50,31 @@ const runFmtFilesSerial = async ( return results; }; +/** Processes files concurrently while preserving input order. */ +const runFmtFilesParallel = async ( + files: FmtFileRequest[], + shouldWrite: boolean, +): Promise => { + const { createFmtWorker } = await import('./parallel.ts'); + const worker = await createFmtWorker(files.length); + + try { + return await Promise.all(files.map((file) => runFmtFile(file, shouldWrite, worker.formatFile))); + } finally { + worker.terminate(); + } +}; + +/** Checks every worker payload before any formatting can begin. */ +const canRunFmtFilesParallel = (files: FmtFileRequest[]): boolean => { + try { + structuredClone(files); + return true; + } catch { + return false; + } +}; + /** Maps file results to the Prettier-compatible CLI exit code. */ const getFmtExitCode = (files: FmtFileResult[]): FmtExitCode => { let exitCode: FmtExitCode = 0; @@ -67,9 +92,17 @@ const getFmtExitCode = (files: FmtFileResult[]): FmtExitCode => { }; /** Runs resolved files and summarizes their outcomes for the CLI. */ -const runFmtFiles = async ({ files, mode }: RunFmtFilesOptions): Promise => { +const runFmtFiles = async ({ + files, + mode, + parallel, +}: RunFmtFilesOptions): Promise => { const startTime = performance.now(); - const results = await runFmtFilesSerial(files, mode === 'write'); + const shouldWrite = mode === 'write'; + const results = + parallel && files.length > 1 && canRunFmtFilesParallel(files) + ? await runFmtFilesParallel(files, shouldWrite) + : await runFmtFilesSerial(files, shouldWrite); return { files: results, diff --git a/packages/rstack/src/fmt/types.ts b/packages/rstack/src/fmt/types.ts index 3bf345b..fba68df 100644 --- a/packages/rstack/src/fmt/types.ts +++ b/packages/rstack/src/fmt/types.ts @@ -54,8 +54,8 @@ interface RunFmtFilesOptions { mode: FmtMode; /** Persistent cache support is added in a later implementation step. */ cache: false; - /** Parallel execution support is added in a later implementation step. */ - parallel: false; + /** Whether cloneable file requests should run in worker threads. */ + parallel: boolean; } interface SuccessfulFmtFileResult { diff --git a/packages/rstack/src/fmt/worker.ts b/packages/rstack/src/fmt/worker.ts index c863342..8739ff7 100644 --- a/packages/rstack/src/fmt/worker.ts +++ b/packages/rstack/src/fmt/worker.ts @@ -1,7 +1,6 @@ -/** - * Internal build entry reserved for the fmt worker. - * - * The worker implementation is intentionally added by a later PR. Keeping a - * dedicated entry now establishes a stable published asset boundary. - */ -export {}; +import { formatFileSerial } from './serial.ts'; + +/** Confirms that the worker module and its runtime dependencies are ready. */ +const initializeFmtWorker = (): true => true; + +export { formatFileSerial, initializeFmtWorker }; diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index 283773f..44d822a 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -84,6 +84,19 @@ test('formats the current directory with Prettier defaults', () => { expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); }); +test('supports disabling parallel execution', () => { + writeProjectFile('first.ts', 'const first="first"'); + writeProjectFile('second.ts', 'const second="second"'); + + const result = runFmt(['--no-parallel', 'first.ts', 'second.ts']); + + expect(result.status).toBe(0); + expect(result.stdout).toBe('first.ts\nsecond.ts\n'); + expect(result.stderr).toBe(''); + expect(readProjectFile('first.ts')).toBe('const first = "first";\n'); + expect(readProjectFile('second.ts')).toBe('const second = "second";\n'); +}); + test('does not load Prettier config or ignore files', () => { writeProjectFile('.prettierrc.json', '{ "singleQuote": true, "semi": false }\n'); writeProjectFile('.prettierignore', 'index.ts\n'); diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index a38a24a..dd57260 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -5,6 +5,7 @@ test('uses write mode by default', () => { expect(parseFmtCLIArgs([])).toEqual({ mode: 'write', patterns: [], + parallel: true, help: false, }); }); @@ -18,6 +19,16 @@ test.each([ expect(parseFmtCLIArgs([option])).toEqual({ mode, patterns: [], + parallel: true, + help: false, + }); +}); + +test.each(['--no-parallel', '--noParallel'])('disables parallel execution with %s', (option) => { + expect(parseFmtCLIArgs([option])).toEqual({ + mode: 'write', + patterns: [], + parallel: false, help: false, }); }); @@ -28,6 +39,7 @@ test('preserves file paths and globs', () => { expect(parseFmtCLIArgs([patterns[0], '--check', ...patterns.slice(1)])).toEqual({ mode: 'check', patterns, + parallel: true, help: false, }); }); @@ -36,6 +48,7 @@ test('treats arguments after the terminator as paths', () => { expect(parseFmtCLIArgs(['--check', '--', '--write', '--help'])).toEqual({ mode: 'check', patterns: ['--write', '--help'], + parallel: true, help: false, }); }); @@ -49,6 +62,7 @@ test('provides command help', () => { expect(fmtHelpMessage).toContain('--write'); expect(fmtHelpMessage).toContain('--check'); expect(fmtHelpMessage).toContain('--list-different'); + expect(fmtHelpMessage).toContain('--no-parallel'); expect(fmtHelpMessage).toContain('-h, --help'); }); @@ -64,7 +78,7 @@ test.each([ ); }); -test.each(['--unknown', '--no-cache', '--no-parallel', '--parallel-workers'])( +test.each(['--unknown', '--no-cache', '--parallel-workers'])( 'rejects unsupported option %s', (option) => { expect(() => parseFmtCLIArgs([option])).toThrow(); diff --git a/packages/rstack/tests/fmt/runner.test.ts b/packages/rstack/tests/fmt/runner.test.ts index 07377e8..52173bc 100644 --- a/packages/rstack/tests/fmt/runner.test.ts +++ b/packages/rstack/tests/fmt/runner.test.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { expect, test } from 'rstack/test'; import { runFmtFiles } from '../../src/fmt/runner.ts'; import type { FmtFileRequest, FmtMode } from '../../src/fmt/types.ts'; -import { withTempProject } from './helpers.ts'; +import { withTempProject, writeProjectFile } from './helpers.ts'; const createRequest = (filePath: string): FmtFileRequest => ({ path: filePath, @@ -13,12 +13,12 @@ const createRequest = (filePath: string): FmtFileRequest => ({ }, }); -const run = (files: FmtFileRequest[], mode: FmtMode = 'write') => +const run = (files: FmtFileRequest[], mode: FmtMode = 'write', parallel = false) => runFmtFiles({ files, mode, cache: false, - parallel: false, + parallel, }); test('does not rewrite unchanged files', async () => { @@ -105,3 +105,37 @@ test('continues after a file fails and gives errors exit-code precedence', async expect(readFileSync(validPath, 'utf8')).toBe('const value=1'); }); }); + +test('parallel execution matches serial results and preserves input order', async () => { + await withTempProject(async (rootPath) => { + const sources = [ + ['changed.ts', 'const changed=1'], + ['invalid.ts', 'const invalid = ;'], + ['unchanged.ts', 'const unchanged = 1;\n'], + ] as const; + + const createRequests = (directory: string) => + sources.map(([name, source]) => + createRequest(writeProjectFile(rootPath, path.join(directory, name), source)), + ); + + const serialResult = await run(createRequests('serial')); + const parallelResult = await run(createRequests('parallel'), 'write', true); + + const summarize = (result: typeof serialResult) => + result.files.map((file) => ({ + name: path.basename(file.path), + status: file.status, + error: file.status === 'error' ? String(file.error) : undefined, + })); + + expect(parallelResult.exitCode).toBe(serialResult.exitCode); + expect(summarize(parallelResult)).toEqual(summarize(serialResult)); + + for (const [name] of sources) { + expect(readFileSync(path.join(rootPath, 'parallel', name), 'utf8')).toBe( + readFileSync(path.join(rootPath, 'serial', name), 'utf8'), + ); + } + }); +}); diff --git a/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts b/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts new file mode 100644 index 0000000..0968bb7 --- /dev/null +++ b/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts @@ -0,0 +1,66 @@ +import { readFileSync } from 'node:fs'; +import { 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'; + +rs.mock('../../src/fmt/parallel.ts', () => ({ + createFmtWorker: () => Promise.reject(new Error('worker startup failed')), +})); + +const createRequest = ( + filePath: string, + plugins?: FmtFileRequest['options']['plugins'], +): FmtFileRequest => ({ + path: filePath, + options: { + filepath: filePath, + parser: 'typescript', + plugins, + }, +}); + +test('does not write files when worker startup fails', async () => { + await withTempProject(async (rootPath) => { + const filePaths = ['first.ts', 'second.ts'].map((name) => + writeProjectFile(rootPath, name, 'const value=1'), + ); + + await expect( + runFmtFiles({ + files: filePaths.map((filePath) => createRequest(filePath)), + mode: 'write', + cache: false, + parallel: true, + }), + ).rejects.toThrow('worker startup failed'); + + for (const filePath of filePaths) { + expect(readFileSync(filePath, 'utf8')).toBe('const value=1'); + } + }); +}); + +test('uses serial execution when options cannot be cloned', async () => { + await withTempProject(async (rootPath) => { + const uncloneablePlugin = { + languages: [], + uncloneable() {}, + }; + const filePaths = ['first.ts', 'second.ts'].map((name) => + writeProjectFile(rootPath, name, 'const value=1'), + ); + + const result = await runFmtFiles({ + files: filePaths.map((filePath) => createRequest(filePath, [uncloneablePlugin])), + mode: 'write', + cache: false, + parallel: true, + }); + + expect(result.files.map((file) => file.status)).toEqual(['written', 'written']); + for (const filePath of filePaths) { + expect(readFileSync(filePath, 'utf8')).toBe('const value = 1;\n'); + } + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 336646c..4c3663a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,9 @@ catalogs: typescript: specifier: ^7.0.2 version: 7.0.2 + worktank: + specifier: 3.0.2 + version: 3.0.2 importers: @@ -385,6 +388,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + worktank: + specifier: 'catalog:' + version: 3.0.2 website: devDependencies: @@ -1500,6 +1506,9 @@ packages: resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} engines: {node: '>= 4'} + immediato@1.1.0: + resolution: {integrity: sha512-6DTWQWiM3SyxAbNRDmMvFgZVwVP6wT8ciQv7GivxXejtXZFIcemC0Wlzfd/jEouJ2JroCIp4qZVloKW4BviUpQ==} + immutable@5.1.9: resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} @@ -1546,6 +1555,12 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + isoconcurrency@1.0.0: + resolution: {integrity: sha512-YhuPf5V6uOtQQHt9gIkOTbq75ceXqraDvxtZZeS/XbNsre6fmM+WpJgNTSkGX5jB3+gnbwoTVqW1c3qdfyVpOA==} + + isotimer@1.0.0: + resolution: {integrity: sha512-1p1wborMl9fFbulXx9YBpIqFnfUn/2tN8Ne9g3GLMaiQAPmN/wLlpNOKCNT822div3Sq7LKkApZJ+6JipDUusQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2291,6 +2306,9 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webworker-shim@1.1.4: + resolution: {integrity: sha512-W/40L5W6ZQyGhYr3hJ7N/2SjdK5OdFtnYm94j6xlRyjckegXnIGwz0EdxdkQx6VGTglJjK8mqBhMz3fd3AY4bg==} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -2298,6 +2316,9 @@ packages: when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + worktank@3.0.2: + resolution: {integrity: sha512-ry5gPtWnakOnUBAAa2aiyWZwAFJuBtd/MwZH6o9DXnQHD4AZvidtl2uTLrb2d3Zjy9D04n84lHJNnIETQl7tuA==} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -3356,6 +3377,8 @@ snapshots: ignore@7.0.6: {} + immediato@1.1.0: {} + immutable@5.1.9: {} indent-string@4.0.0: {} @@ -3391,6 +3414,12 @@ snapshots: is-plain-obj@4.1.0: {} + isoconcurrency@1.0.0: {} + + isotimer@1.0.0: + dependencies: + immediato: 1.1.0 + js-tokens@4.0.0: {} json5@2.2.3: {} @@ -4452,10 +4481,19 @@ snapshots: web-namespaces@2.0.1: {} + webworker-shim@1.1.4: {} + whatwg-mimetype@3.0.0: {} when-exit@2.1.5: {} + worktank@3.0.2: + dependencies: + isoconcurrency: 1.0.0 + isotimer: 1.0.0 + promise-make-naked: 3.0.2 + webworker-shim: 1.1.4 + ws@8.21.0: {} yaml@2.9.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 73e8a96..45e93d8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -51,6 +51,7 @@ catalog: 'rspress-plugin-font-open-sans': '^1.0.4' tiny-readdir: 3.1.1 'typescript': '^7.0.2' + worktank: '3.0.2' dedupePeers: true From d1915908fea6f8339fb25719569bf05786a9815d Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 1 Aug 2026 17:49:38 +0800 Subject: [PATCH 2/2] chore(fmt): simplify upstream attribution --- packages/rstack/src/fmt/parallel.ts | 6 +----- packages/rstack/src/fmt/serial.ts | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/rstack/src/fmt/parallel.ts b/packages/rstack/src/fmt/parallel.ts index 4223df7..8907a05 100644 --- a/packages/rstack/src/fmt/parallel.ts +++ b/packages/rstack/src/fmt/parallel.ts @@ -1,8 +1,4 @@ -/** - * Derived from @prettier/cli v0.12.0. - * SPDX-License-Identifier: MIT - * Modified by Rstack contributors. - */ +// Derived from @prettier/cli, see THIRD_PARTY_NOTICES.md import { availableParallelism } from 'node:os'; import WorkTank from 'worktank'; diff --git a/packages/rstack/src/fmt/serial.ts b/packages/rstack/src/fmt/serial.ts index 57abcfc..acd90c1 100644 --- a/packages/rstack/src/fmt/serial.ts +++ b/packages/rstack/src/fmt/serial.ts @@ -1,8 +1,4 @@ -/** - * Derived from @prettier/cli v0.12.0. - * SPDX-License-Identifier: MIT - * Modified by Rstack contributors. - */ +// Derived from @prettier/cli, see THIRD_PARTY_NOTICES.md import { readFile, writeFile } from 'atomically'; import { format } from 'prettier';