diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 663c73b5..ead4501e 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -140,7 +140,6 @@ const runFmtCLI = async (args: string[]): Promise => { const result = await runFmtFiles({ files, mode, - cache: false, maxWorkers, }); diff --git a/packages/rstack/src/fmt/format.ts b/packages/rstack/src/fmt/format.ts deleted file mode 100644 index b2898316..00000000 --- a/packages/rstack/src/fmt/format.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { format, formatWithCursor } from 'prettier'; -import { resolveFmtOptions } from './config.ts'; -import { resolveFmtParser } from './parser.ts'; -import { createFmtPluginResolver } from './plugins.ts'; -import { getPrettierPlugins } from './prettierPlugins.ts'; -import type { FormatTextOptions, FormatTextResult } from './types.ts'; - -/** Formats source text without reading formatter config or ignore files. */ -const formatText = async ( - source: string, - { filePath, cursorOffset, config }: FormatTextOptions, -): Promise => { - const options = createFmtPluginResolver(config.rootPath)(resolveFmtOptions(filePath, config)); - const formatOptions = { - ...options, - filepath: filePath, - }; - const plugins = await getPrettierPlugins(formatOptions); - const parser = await resolveFmtParser(filePath, formatOptions, plugins); - - if (!parser) { - return { - status: 'skipped', - reason: 'unsupported', - }; - } - - const resolvedOptions = { ...formatOptions, parser, plugins }; - - if (cursorOffset === undefined) { - return { - status: 'formatted', - formatted: await format(source, resolvedOptions), - }; - } - - const result = await formatWithCursor(source, { - ...resolvedOptions, - cursorOffset, - }); - - return { - status: 'formatted', - formatted: result.formatted, - cursorOffset: result.cursorOffset, - }; -}; - -export { formatText }; diff --git a/packages/rstack/src/fmt/parser.ts b/packages/rstack/src/fmt/parser.ts deleted file mode 100644 index f841220f..00000000 --- a/packages/rstack/src/fmt/parser.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getFileInfo, type FileInfoOptions, type Options as PrettierOptions } from 'prettier'; - -type PrettierPlugins = NonNullable; - -const fileInfoOptions = { - ignorePath: [], - resolveConfig: false, - withNodeModules: true, -} satisfies FileInfoOptions; - -/** Uses the configured parser or infers one without loading Prettier config. */ -const resolveFmtParser = async ( - filePath: string, - options: PrettierOptions, - plugins: PrettierPlugins, -): Promise => - options.parser ?? - ( - await getFileInfo(filePath, { - ...fileInfoOptions, - plugins, - }) - ).inferredParser; - -export { resolveFmtParser }; diff --git a/packages/rstack/src/fmt/runner.ts b/packages/rstack/src/fmt/runner.ts index a0fa109b..76858e28 100644 --- a/packages/rstack/src/fmt/runner.ts +++ b/packages/rstack/src/fmt/runner.ts @@ -3,12 +3,12 @@ import type { FmtFileRequest, FmtFileResult, FmtRunResult, - FmtWorkerFileResult, RunFmtFilesOptions, } from './types.ts'; +import type { FmtWorkerPool } from './workerPool.ts'; /** Formats one file and reports whether its contents differ. */ -type FormatFile = (file: FmtFileRequest, shouldWrite: boolean) => Promise; +type FormatFile = FmtWorkerPool['formatFile']; /** Converts a formatter outcome into the shared per-file result. */ const runFmtFile = async ( @@ -39,22 +39,22 @@ const runFmtFile = async ( } }; -/** Processes files in workers while preserving input order. */ -const runFmtFilesWithWorkers = async ( +/** Processes files in a worker pool while preserving input order. */ +const runFmtFilesInWorkerPool = async ( files: FmtFileRequest[], shouldWrite: boolean, maxWorkers?: number, ): Promise => { - const { createFmtWorker } = await import('./parallel.ts'); - const worker = await createFmtWorker(files.length, maxWorkers); + const { createFmtWorkerPool } = await import('./workerPool.ts'); + const workerPool = await createFmtWorkerPool(files.length, maxWorkers); try { const results = await Promise.all( - files.map((file) => runFmtFile(file, shouldWrite, worker.formatFile)), + files.map((file) => runFmtFile(file, shouldWrite, workerPool.formatFile)), ); return results.filter((result): result is FmtFileResult => result !== undefined); } finally { - worker.terminate(); + workerPool.terminate(); } }; @@ -83,7 +83,7 @@ const runFmtFiles = async ({ const startTime = performance.now(); const shouldWrite = mode === 'write'; const results = - files.length === 0 ? [] : await runFmtFilesWithWorkers(files, shouldWrite, maxWorkers); + files.length === 0 ? [] : await runFmtFilesInWorkerPool(files, shouldWrite, maxWorkers); return { files: results, diff --git a/packages/rstack/src/fmt/types.ts b/packages/rstack/src/fmt/types.ts index 7be9afa9..00282ee1 100644 --- a/packages/rstack/src/fmt/types.ts +++ b/packages/rstack/src/fmt/types.ts @@ -45,15 +45,6 @@ interface ResolvedFmtConfig { ignorePatterns: string[]; } -interface FormatTextOptions { - /** File path used to resolve per-file options and infer the parser. */ - filePath: string; - /** Cursor offset in the source to preserve across formatting. */ - cursorOffset?: number; - /** Resolved project config used to derive per-file options. */ - config: ResolvedFmtConfig; -} - interface DiscoverFmtFilesOptions { /** Absolute directory used to resolve input paths. */ cwd: string; @@ -70,7 +61,6 @@ interface FmtFileRequest { options: ResolvedFmtOptions & Required>; } -type FmtWorkerFileResult = 'changed' | 'unchanged' | 'unsupported'; type FmtMode = 'write' | 'check' | 'list-different'; type FmtExitCode = 0 | 1 | 2; @@ -79,8 +69,6 @@ interface RunFmtFilesOptions { files: FmtFileRequest[]; /** Whether to write changes or only report them. */ mode: FmtMode; - /** Persistent cache support is added in a later implementation step. */ - cache: false; /** Maximum number of formatting workers. */ maxWorkers?: number; } @@ -107,19 +95,6 @@ interface FmtRunResult { durationMs: number; } -interface FormattedTextResult { - status: 'formatted'; - formatted: string; - cursorOffset?: number; -} - -interface SkippedTextResult { - status: 'skipped'; - reason: 'unsupported'; -} - -type FormatTextResult = FormattedTextResult | SkippedTextResult; - export type { DiscoverFmtFilesOptions, FmtConfig, @@ -130,9 +105,6 @@ export type { FmtMode, FmtPluginSpecifier, FmtRunResult, - FmtWorkerFileResult, - FormatTextOptions, - FormatTextResult, ResolvedFmtConfig, ResolvedFmtOptions, RunFmtFilesOptions, diff --git a/packages/rstack/src/fmt/worker.ts b/packages/rstack/src/fmt/worker.ts index ed81301e..445de6ff 100644 --- a/packages/rstack/src/fmt/worker.ts +++ b/packages/rstack/src/fmt/worker.ts @@ -1,10 +1,37 @@ // Derived from @prettier/cli, see THIRD_PARTY_NOTICES.md import { readFileSync, writeFileSync } from 'node:fs'; -import { format } from 'prettier'; -import { resolveFmtParser } from './parser.ts'; +import { + format, + getFileInfo, + type FileInfoOptions, + type Options as PrettierOptions, +} from 'prettier'; import { getPrettierPlugins } from './prettierPlugins.ts'; -import type { FmtFileRequest, FmtWorkerFileResult } from './types.ts'; +import type { FmtFileRequest } from './types.ts'; + +type PrettierPlugins = NonNullable; +type FormatFileResult = 'changed' | 'unchanged' | 'unsupported'; + +const fileInfoOptions = { + ignorePath: [], + resolveConfig: false, + withNodeModules: true, +} satisfies FileInfoOptions; + +/** Uses the configured parser or infers one without loading Prettier config. */ +const resolveFmtParser = async ( + filePath: string, + options: PrettierOptions, + plugins: PrettierPlugins, +): Promise => + options.parser ?? + ( + await getFileInfo(filePath, { + ...fileInfoOptions, + plugins, + }) + ).inferredParser; /** * Use synchronous direct I/O inside the dedicated worker to avoid libuv @@ -13,7 +40,7 @@ import type { FmtFileRequest, FmtWorkerFileResult } from './types.ts'; const formatFile = async ( { path, options }: FmtFileRequest, shouldWrite: boolean, -): Promise => { +): Promise => { const plugins = await getPrettierPlugins(options); const parser = await resolveFmtParser(path, options, plugins); if (!parser) { diff --git a/packages/rstack/src/fmt/parallel.ts b/packages/rstack/src/fmt/workerPool.ts similarity index 87% rename from packages/rstack/src/fmt/parallel.ts rename to packages/rstack/src/fmt/workerPool.ts index 886a0dda..a4fd851f 100644 --- a/packages/rstack/src/fmt/parallel.ts +++ b/packages/rstack/src/fmt/workerPool.ts @@ -5,7 +5,7 @@ import WorkTank from 'worktank'; type FmtWorkerMethods = typeof import('./worker.ts'); -interface FmtWorker { +interface FmtWorkerPool { formatFile: FmtWorkerMethods['formatFile']; terminate: () => void; } @@ -22,7 +22,10 @@ const getFmtWorkerUrl = (): URL => { }; /** Creates and starts every worker before formatting can begin. */ -const createFmtWorker = async (fileCount: number, maxWorkers?: number): Promise => { +const createFmtWorkerPool = async ( + fileCount: number, + maxWorkers?: number, +): Promise => { const workerCount = getFmtWorkerCount(fileCount, maxWorkers); const pool = new WorkTank({ pool: { @@ -51,4 +54,5 @@ const createFmtWorker = async (fileCount: number, maxWorkers?: number): Promise< }; }; -export { createFmtWorker, getFmtWorkerCount }; +export { createFmtWorkerPool, getFmtWorkerCount }; +export type { FmtWorkerPool }; diff --git a/packages/rstack/tests/fmt/format.test.ts b/packages/rstack/tests/fmt/format.test.ts deleted file mode 100644 index 6b7faed7..00000000 --- a/packages/rstack/tests/fmt/format.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import path from 'node:path'; -import { expect, test } from 'rstack/test'; -import { normalizeFmtConfig } from '../../src/fmt/config.ts'; -import { formatText } from '../../src/fmt/format.ts'; -import { withTempProject, writeProjectFile } from './helpers.ts'; - -const rootPath = import.meta.dirname; -const packageJsonSource = - '{"dependencies":{"z":"1.0.0","a":"1.0.0"},"version":"1.0.0","name":"fixture"}'; - -test('applies per-file overrides and maps the cursor', async () => { - const source = 'const value={message:"hello"}'; - const config = normalizeFmtConfig( - { - overrides: [ - { - files: '*.ts', - options: { - singleQuote: true, - }, - }, - ], - }, - rootPath, - ); - - const result = await formatText(source, { - config, - cursorOffset: source.indexOf('message'), - filePath: path.join(rootPath, 'example.ts'), - }); - - expect(result).toEqual({ - status: 'formatted', - formatted: "const value = { message: 'hello' };\n", - cursorOffset: 16, - }); -}); - -test('returns unsupported when no parser can be inferred', async () => { - const config = normalizeFmtConfig(undefined, rootPath); - - await expect( - formatText('plain text', { - config, - filePath: path.join(rootPath, 'unknown.extension'), - }), - ).resolves.toEqual({ - status: 'skipped', - reason: 'unsupported', - }); -}); - -test('uses an explicit parser for unknown file extensions', async () => { - const config = normalizeFmtConfig({ parser: 'babel' }, rootPath); - - const result = await formatText('const value={nested:true}', { - config, - filePath: path.join(rootPath, 'unknown.extension'), - }); - - expect(result).toEqual({ - status: 'formatted', - formatted: 'const value = { nested: true };\n', - }); -}); - -test('does not sort package.json by default', async () => { - const result = await formatText(packageJsonSource, { - config: normalizeFmtConfig(undefined, rootPath), - filePath: path.join(rootPath, 'package.json'), - }); - - expect(result).toMatchObject({ - formatted: - '{\n "dependencies": {\n "z": "1.0.0",\n "a": "1.0.0"\n },\n "version": "1.0.0",\n "name": "fixture"\n}\n', - }); -}); - -test('sorts package.json when enabled', async () => { - const result = await formatText(packageJsonSource, { - config: normalizeFmtConfig( - { - overrides: [{ files: 'package.json', options: { sortPackageJson: true } }], - }, - rootPath, - ), - filePath: path.join(rootPath, 'package.json'), - }); - - expect(result).toMatchObject({ - formatted: - '{\n "name": "fixture",\n "version": "1.0.0",\n "dependencies": {\n "a": "1.0.0",\n "z": "1.0.0"\n }\n}\n', - }); -}); - -test('supports a plugin path from matching overrides', async () => { - await withTempProject(async (projectPath) => { - writeProjectFile( - projectPath, - 'plugins/fixture.mjs', - `export default { - languages: [{ name: 'Fixture JSON', parsers: ['json'], extensions: ['.fixture'] }], -}; -`, - ); - const config = normalizeFmtConfig( - { - overrides: [ - { - files: '*.fixture', - options: { plugins: ['./plugins/fixture.mjs'] }, - }, - ], - }, - projectPath, - ); - - const result = await formatText('{"value":true}', { - config, - filePath: path.join(projectPath, 'example.fixture'), - }); - - expect(result).toEqual({ - status: 'formatted', - formatted: '{ "value": true }\n', - }); - }); -}); - -test('formats an explicitly provided node_modules file', async () => { - const config = normalizeFmtConfig(undefined, rootPath); - - const result = await formatText('const value={nested:true}', { - config, - filePath: path.join(rootPath, 'node_modules', 'example', 'index.js'), - }); - - expect(result).toEqual({ - status: 'formatted', - formatted: 'const value = { nested: true };\n', - }); -}); diff --git a/packages/rstack/tests/fmt/runner.test.ts b/packages/rstack/tests/fmt/runner.test.ts index ba8f018d..40290d53 100644 --- a/packages/rstack/tests/fmt/runner.test.ts +++ b/packages/rstack/tests/fmt/runner.test.ts @@ -17,7 +17,6 @@ const run = (files: FmtFileRequest[], mode: FmtMode = 'write') => runFmtFiles({ files, mode, - cache: false, }); test('does not rewrite unchanged files', async () => { diff --git a/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts similarity index 60% rename from packages/rstack/tests/fmt/runnerParallelPreflight.test.ts rename to packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts index 18bf2b1f..89129fac 100644 --- a/packages/rstack/tests/fmt/runnerParallelPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts @@ -5,18 +5,18 @@ import type { FmtFileRequest } from '../../src/fmt/types.ts'; import { withTempProject, writeProjectFile } from './helpers.ts'; const mocks = rs.hoisted(() => ({ - createFmtWorkerCalls: [] as [number, number | undefined][], + createFmtWorkerPoolCalls: [] as [number, number | undefined][], })); -rs.mock('../../src/fmt/parallel.ts', () => ({ - createFmtWorker: (fileCount: number, maxWorkers?: number) => { - mocks.createFmtWorkerCalls.push([fileCount, maxWorkers]); +rs.mock('../../src/fmt/workerPool.ts', () => ({ + createFmtWorkerPool: (fileCount: number, maxWorkers?: number) => { + mocks.createFmtWorkerPoolCalls.push([fileCount, maxWorkers]); return Promise.reject(new Error('worker startup failed')); }, })); beforeEach(() => { - mocks.createFmtWorkerCalls.length = 0; + mocks.createFmtWorkerPoolCalls.length = 0; }); const createRequest = (filePath: string): FmtFileRequest => ({ @@ -27,7 +27,7 @@ const createRequest = (filePath: string): FmtFileRequest => ({ }, }); -test('starts a worker before formatting a single file', async () => { +test('starts the worker pool before formatting a single file', async () => { await withTempProject(async (rootPath) => { const filePath = writeProjectFile(rootPath, 'index.ts', 'const value=1'); @@ -35,20 +35,19 @@ test('starts a worker before formatting a single file', async () => { runFmtFiles({ files: [createRequest(filePath)], mode: 'write', - cache: false, maxWorkers: 1, }), ).rejects.toThrow('worker startup failed'); - expect(mocks.createFmtWorkerCalls).toEqual([[1, 1]]); + expect(mocks.createFmtWorkerPoolCalls).toEqual([[1, 1]]); expect(readFileSync(filePath, 'utf8')).toBe('const value=1'); }); }); -test('does not start a worker when there are no files', async () => { - await expect(runFmtFiles({ files: [], mode: 'write', cache: false })).resolves.toMatchObject({ +test('does not start the worker pool when there are no files', async () => { + await expect(runFmtFiles({ files: [], mode: 'write' })).resolves.toMatchObject({ files: [], exitCode: 0, }); - expect(mocks.createFmtWorkerCalls).toEqual([]); + expect(mocks.createFmtWorkerPoolCalls).toEqual([]); }); diff --git a/packages/rstack/tests/fmt/runnerWriteFailure.test.ts b/packages/rstack/tests/fmt/runnerWriteFailure.test.ts index 298c0bb3..33657a68 100644 --- a/packages/rstack/tests/fmt/runnerWriteFailure.test.ts +++ b/packages/rstack/tests/fmt/runnerWriteFailure.test.ts @@ -5,8 +5,8 @@ const mocks = rs.hoisted(() => ({ terminateCalls: 0, })); -rs.mock('../../src/fmt/parallel.ts', () => ({ - createFmtWorker: () => +rs.mock('../../src/fmt/workerPool.ts', () => ({ + createFmtWorkerPool: () => Promise.resolve({ formatFile: () => Promise.reject(new Error('file write failed')), terminate: () => { @@ -29,7 +29,6 @@ test('returns an error when a file write fails', async () => { }, ], mode: 'write', - cache: false, }); expect(result).toMatchObject({ diff --git a/packages/rstack/tests/fmt/worker.test.ts b/packages/rstack/tests/fmt/worker.test.ts index 1d6a99bb..ecf39d17 100644 --- a/packages/rstack/tests/fmt/worker.test.ts +++ b/packages/rstack/tests/fmt/worker.test.ts @@ -25,10 +25,10 @@ test('writes formatted files', async () => { }); }); -test('infers the parser before formatting', async () => { +test('infers the parser for an explicitly provided node_modules file', async () => { await withTempProject(async (rootPath) => { const source = 'const value=1'; - const filePath = writeProjectFile(rootPath, 'example.ts', source); + const filePath = writeProjectFile(rootPath, 'node_modules/example/index.ts', source); await expect( formatFile( diff --git a/packages/rstack/tests/fmt/parallel.test.ts b/packages/rstack/tests/fmt/workerPool.test.ts similarity index 89% rename from packages/rstack/tests/fmt/parallel.test.ts rename to packages/rstack/tests/fmt/workerPool.test.ts index 977bbf4e..7dcd83fb 100644 --- a/packages/rstack/tests/fmt/parallel.test.ts +++ b/packages/rstack/tests/fmt/workerPool.test.ts @@ -1,6 +1,6 @@ import { availableParallelism } from 'node:os'; import { expect, test } from 'rstack/test'; -import { getFmtWorkerCount } from '../../src/fmt/parallel.ts'; +import { getFmtWorkerCount } from '../../src/fmt/workerPool.ts'; test('uses one fewer worker than the available parallelism by default', () => { const defaultWorkerCount = Math.max(1, availableParallelism() - 1);