diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 55af2173..7a11d4da 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -366,5 +366,4 @@ const runFmtCLI = async (args: string[]): Promise => { } }; -export { fmtHelpMessage, parseFmtCLIArgs, prettyTime, runFmtCLI }; -export type { ParsedFmtCLIArgs }; +export { runFmtCLI }; diff --git a/packages/rstack/src/fmt/workerPool.ts b/packages/rstack/src/fmt/workerPool.ts index e350a149..7a569636 100644 --- a/packages/rstack/src/fmt/workerPool.ts +++ b/packages/rstack/src/fmt/workerPool.ts @@ -64,5 +64,5 @@ const createFmtWorkerPool = async ( }; }; -export { createFmtWorkerPool, getFmtWorkerCount }; +export { createFmtWorkerPool }; export type { FmtWorkerPool }; diff --git a/packages/rstack/src/projectCache.ts b/packages/rstack/src/projectCache.ts index 5959b1ce..86d0ffcb 100644 --- a/packages/rstack/src/projectCache.ts +++ b/packages/rstack/src/projectCache.ts @@ -31,5 +31,5 @@ const ensureProjectCacheDir = async (rootPath: string): Promise Path to an additional ignore file (repeatable) - -u, --ignore-unknown Ignore unknown files - --no-cache Disable the formatting cache - --cache-location Path to the formatting cache directory - --no-error-on-unmatched-pattern Do not error when no files match - --with-node-modules Process files inside node_modules - --parallel-workers Number of parallel workers - --stdin-filepath Format stdin as if it were saved at - -h, --help Display this help message" -`; diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts deleted file mode 100644 index 5bd45d27..00000000 --- a/packages/rstack/tests/fmt/cli.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { stripVTControlCharacters } from 'node:util'; -import { expect, test } from 'rstack/test'; -import { fmtHelpMessage, parseFmtCLIArgs, prettyTime } from '../../src/fmt/cli.ts'; - -test.each([ - [0, '0.000s'], - [0.009, '0.009s'], - [0.01, '0.01s'], - [9.876, '9.88s'], - [10, '10.0s'], - [59.9, '59.9s'], - [60, '1m'], - [61, '1m 1s'], - [61.25, '1m 1.3s'], - [125.25, '2m 5.3s'], -] as const)('formats %s seconds as %s', (seconds, expected) => { - expect(stripVTControlCharacters(prettyTime(seconds))).toBe(expected); -}); - -test('uses write mode by default', () => { - expect(parseFmtCLIArgs([])).toEqual({ - cache: true, - mode: 'write', - patterns: [], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: undefined, - help: false, - }); -}); - -test.each([ - ['-w', 'write'], - ['--write', 'write'], - ['--check', 'check'], - ['-l', 'list-different'], - ['--list-different', 'list-different'], -] as const)('parses %s mode', (option, mode) => { - expect(parseFmtCLIArgs([option])).toEqual({ - cache: true, - mode, - patterns: [], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: undefined, - help: false, - }); -}); - -test('configures parallel worker count', () => { - expect(parseFmtCLIArgs(['--parallel-workers', '3'])).toEqual({ - cache: true, - mode: 'write', - patterns: [], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: 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('preserves file paths and globs', () => { - const patterns = ['src/file with spaces.ts', 'src/**/*.{js,ts}', '!src/generated/**']; - - expect(parseFmtCLIArgs([patterns[0], '--check', ...patterns.slice(1)])).toEqual({ - cache: true, - mode: 'check', - patterns, - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: undefined, - help: false, - }); -}); - -test('treats arguments after the terminator as paths', () => { - expect(parseFmtCLIArgs(['--check', '--', '--write', '--help'])).toEqual({ - cache: true, - mode: 'check', - patterns: ['--write', '--help'], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: undefined, - help: false, - }); -}); - -test.each(['--help', '-h'])('parses %s', (option) => { - expect(parseFmtCLIArgs([option]).help).toBe(true); -}); - -test('collects repeated ignore paths', () => { - expect( - parseFmtCLIArgs(['--ignore-path', '.prettierignore', '--ignore-path=config/format.ignore']) - .ignorePaths, - ).toEqual(['.prettierignore', 'config/format.ignore']); -}); - -test('parses --no-error-on-unmatched-pattern', () => { - expect(parseFmtCLIArgs(['--no-error-on-unmatched-pattern']).noErrorOnUnmatchedPattern).toBe(true); -}); - -test.each(['-u', '--ignore-unknown', '--ignoreUnknown'])('parses %s', (option) => { - expect(parseFmtCLIArgs([option]).ignoreUnknown).toBe(true); -}); - -test('parses --no-cache', () => { - expect(parseFmtCLIArgs(['--no-cache']).cache).toBe(false); -}); - -test('parses --cache-location', () => { - expect(parseFmtCLIArgs(['--cache-location', '.cache/fmt']).cacheLocation).toBe('.cache/fmt'); -}); - -test('--no-cache ignores --cache-location', () => { - expect(parseFmtCLIArgs(['--no-cache', '--cache-location='])).toMatchObject({ - cache: false, - cacheLocation: undefined, - }); -}); - -test('rejects an empty cache location', () => { - expect(() => parseFmtCLIArgs(['--cache-location='])).toThrow( - 'The --cache-location option requires a path.', - ); -}); - -test('parses --with-node-modules', () => { - expect(parseFmtCLIArgs(['--with-node-modules']).withNodeModules).toBe(true); -}); - -test('parses --stdin-filepath', () => { - expect(parseFmtCLIArgs(['--stdin-filepath', 'src/index.ts'])).toEqual({ - cache: true, - mode: 'write', - patterns: [], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: undefined, - help: false, - stdinFilepath: 'src/index.ts', - }); -}); - -test('accepts a worker count with --stdin-filepath', () => { - expect(parseFmtCLIArgs(['--stdin-filepath', 'index.ts', '--parallel-workers', '2'])).toEqual({ - cache: true, - mode: 'write', - patterns: [], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: 2, - help: false, - stdinFilepath: 'index.ts', - }); -}); - -test.each(['--write', '--check', '--list-different'])( - 'rejects %s with --stdin-filepath', - (option) => { - expect(() => parseFmtCLIArgs(['--stdin-filepath', 'index.ts', option])).toThrow( - 'The --stdin-filepath option cannot be used with --write, --check, or --list-different.', - ); - }, -); - -test('rejects file arguments with --stdin-filepath', () => { - expect(() => parseFmtCLIArgs(['--stdin-filepath', 'index.ts', 'src/other.ts'])).toThrow( - 'The --stdin-filepath option cannot be used with file arguments.', - ); -}); - -test('provides command help', () => { - const helpMessage = stripVTControlCharacters(fmtHelpMessage).replace(/^Rstack v.*\n\n/, ''); - - expect(helpMessage).toContain('Usage:\n $ rs fmt [options] [files/globs...]'); - expect(helpMessage).toMatchSnapshot(); -}); - -test.each([ - ['--write', '--check'], - ['--write', '--list-different'], - ['--check', '--list-different'], - ['--write', '--check', '--list-different'], -])('rejects conflicting modes: %s', (...args) => { - expect(() => parseFmtCLIArgs(args)).toThrow( - 'The --write, --check, and --list-different options cannot be used together.', - ); -}); diff --git a/packages/rstack/tests/fmt/format.test.ts b/packages/rstack/tests/fmt/format.test.ts deleted file mode 100644 index e8a7abe2..00000000 --- a/packages/rstack/tests/fmt/format.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import path from 'node:path'; -import { expect, test } from 'rstack/test'; -import { formatFmtSource } from '../../src/fmt/format.ts'; - -const rootPath = path.join(import.meta.dirname, 'fixture'); - -test('formats sources without touching the file system', async () => { - await expect( - formatFmtSource( - { path: path.join(rootPath, 'missing.ts'), options: {} }, - () => 'const value=1', - ), - ).resolves.toEqual({ - status: 'formatted', - source: 'const value=1', - formatted: 'const value = 1;\n', - }); -}); - -test('applies resolved options to the source', async () => { - const result = await formatFmtSource( - { path: path.join(rootPath, 'missing.ts'), options: { singleQuote: true, semi: false } }, - () => 'const message="hello"', - ); - - expect(result).toEqual({ - status: 'formatted', - source: 'const message="hello"', - formatted: "const message = 'hello'\n", - }); -}); - -test('sorts package.json when the option is enabled', async () => { - const result = await formatFmtSource( - { path: path.join(rootPath, 'package.json'), options: { sortPackageJson: true } }, - () => '{"version":"1.0.0","name":"fixture"}', - ); - - expect(result).toEqual({ - status: 'formatted', - source: '{"version":"1.0.0","name":"fixture"}', - formatted: '{\n "name": "fixture",\n "version": "1.0.0"\n}\n', - }); -}); - -test('reports unsupported files before reading the source', async () => { - let read = false; - - await expect( - formatFmtSource({ path: path.join(rootPath, 'missing.unknown'), options: {} }, () => { - read = true; - return ''; - }), - ).resolves.toEqual({ status: 'unsupported' }); - expect(read).toBe(false); -}); - -test('rejects sources that cannot be parsed', async () => { - await expect( - formatFmtSource({ path: path.join(rootPath, 'invalid.ts'), options: {} }, () => 'const x = ;'), - ).rejects.toThrow("Unexpected token ';'"); -}); diff --git a/packages/rstack/tests/fmt/pathHelpers.test.ts b/packages/rstack/tests/fmt/pathHelpers.test.ts deleted file mode 100644 index e1b1a2c6..00000000 --- a/packages/rstack/tests/fmt/pathHelpers.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import path from 'node:path'; -import { expect, test } from 'rstack/test'; -import { createRelativePathResolver, toPosixPath } from '../../src/fmt/pathHelpers.ts'; - -const rootPath = path.join(import.meta.dirname, 'project'); - -test('converts platform paths to POSIX paths', () => { - expect(toPosixPath(path.join('src', 'index.ts'))).toBe('src/index.ts'); -}); - -test('resolves paths relative to a fixed root', () => { - const resolveRelativePath = createRelativePathResolver(rootPath); - - expect(resolveRelativePath(rootPath)).toBe(''); - expect(resolveRelativePath(path.join(rootPath, 'src/index.ts'))).toBe( - path.join('src', 'index.ts'), - ); -}); - -test('falls back for paths outside the fixed root', () => { - const resolveRelativePath = createRelativePathResolver(rootPath); - const siblingPath = path.join(`${rootPath}-other`, 'index.ts'); - - expect(resolveRelativePath(siblingPath)).toBe(path.relative(rootPath, siblingPath)); -}); diff --git a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts index 640fe832..9d21cb4c 100644 --- a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts @@ -1,4 +1,3 @@ -import { readFileSync } from 'node:fs'; import path from 'node:path'; import { beforeEach, expect, rs, test } from 'rstack/test'; import { cacheNamespace, createOptionsHasher } from '../../src/fmt/cacheIdentity.ts'; @@ -22,39 +21,6 @@ beforeEach(() => { mocks.createFmtWorkerPoolCalls.length = 0; }); -const createRequest = (filePath: string): FmtFileRequest => ({ - path: filePath, - options: { - parser: 'typescript', - }, -}); - -test('starts the worker pool before formatting a single file', async () => { - await withTempProject(async (rootPath) => { - const filePath = writeProjectFile(rootPath, 'index.ts', 'const value=1'); - - await expect( - runFmtFiles({ - files: [createRequest(filePath)], - mode: 'write', - maxWorkers: 1, - }), - ).rejects.toThrow('worker startup failed'); - - expect(mocks.createFmtWorkerPoolCalls).toEqual([[1, 1]]); - expect(readFileSync(filePath, 'utf8')).toBe('const value=1'); - }); -}); - -test('does not start the worker pool when there are no files', async () => { - await expect(runFmtFiles({ files: [], mode: 'write' })).resolves.toMatchObject({ - files: [], - exitCode: 0, - processedFileCount: 0, - }); - expect(mocks.createFmtWorkerPoolCalls).toEqual([]); -}); - test('does not start the worker pool when every parser result is cached as unsupported', async () => { await withTempProject(async (rootPath) => { const filePath = writeProjectFile(rootPath, 'example.unknown', 'plain text'); diff --git a/packages/rstack/tests/fmt/runnerWriteFailure.test.ts b/packages/rstack/tests/fmt/runnerWriteFailure.test.ts deleted file mode 100644 index efb96dfd..00000000 --- a/packages/rstack/tests/fmt/runnerWriteFailure.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { expect, rs, test } from 'rstack/test'; -import { runFmtFiles } from '../../src/fmt/runner.ts'; - -const mocks = rs.hoisted(() => ({ - terminateCalls: 0, -})); - -rs.mock('../../src/fmt/workerPool.ts', () => ({ - createFmtWorkerPool: () => - Promise.resolve({ - workerCount: 1, - formatFile: () => Promise.reject(new Error('file write failed')), - terminate: () => { - mocks.terminateCalls++; - return Promise.resolve(); - }, - }), -})); - -test('returns an error when a file write fails', async () => { - const filePath = '/virtual/example.ts'; - - const result = await runFmtFiles({ - files: [ - { - path: filePath, - options: { - parser: 'typescript', - }, - }, - ], - mode: 'write', - }); - - expect(result).toMatchObject({ - exitCode: 2, - files: [ - { - path: filePath, - status: 'error', - error: { message: 'file write failed' }, - }, - ], - processedFileCount: 1, - }); - expect(mocks.terminateCalls).toBe(1); -}); diff --git a/packages/rstack/tests/fmt/worker.test.ts b/packages/rstack/tests/fmt/worker.test.ts index 29b0a901..6f3631d5 100644 --- a/packages/rstack/tests/fmt/worker.test.ts +++ b/packages/rstack/tests/fmt/worker.test.ts @@ -5,45 +5,6 @@ import { sha256 } from '../../src/fmt/cacheIdentity.ts'; import { formatFile } from '../../src/fmt/worker.ts'; import { withTempProject, writeProjectFile } from './helpers.ts'; -test('writes formatted files', async () => { - await withTempProject(async (rootPath) => { - const filePath = writeProjectFile(rootPath, 'example.ts', 'const value=1'); - - await expect( - formatFile({ - file: { - path: filePath, - options: { - parser: 'typescript', - }, - }, - shouldWrite: true, - }), - ).resolves.toEqual({ status: 'changed' }); - - expect(readFileSync(filePath, 'utf8')).toBe('const value = 1;\n'); - }); -}); - -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, 'node_modules/example/index.ts', source); - - await expect( - formatFile({ - file: { - path: filePath, - options: {}, - }, - shouldWrite: false, - }), - ).resolves.toEqual({ status: 'changed' }); - - expect(readFileSync(filePath, 'utf8')).toBe(source); - }); -}); - test('returns cached states before resolving the parser', async () => { await withTempProject(async (rootPath) => { const source = 'const value=1'; diff --git a/packages/rstack/tests/fmt/workerPool.test.ts b/packages/rstack/tests/fmt/workerPool.test.ts deleted file mode 100644 index 39637c0b..00000000 --- a/packages/rstack/tests/fmt/workerPool.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { expect, test } from 'rstack/test'; -import { getFmtWorkerCount } from '../../src/fmt/workerPool.ts'; - -test.each([ - [4, 1, 1], - [4, 2, 2], - [2, 4, 2], - [12, 10, 10], -])('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/projectCache.test.ts b/packages/rstack/tests/projectCache.test.ts deleted file mode 100644 index eff83131..00000000 --- a/packages/rstack/tests/projectCache.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { existsSync, readFileSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import { expect, test } from 'rstack/test'; -import { ensureProjectCacheDir, getProjectCacheDir } from '../src/projectCache.ts'; -import { withTempProject, writeProjectFile } from './fmt/helpers.ts'; - -test('creates and repairs an ignored project cache only when requested', async () => { - await withTempProject(async (rootPath) => { - const cachePath = getProjectCacheDir(rootPath); - const ignorePath = path.join(cachePath, '.gitignore'); - - expect(cachePath).toBe(path.join(rootPath, '.rstack', 'cache')); - expect(existsSync(cachePath)).toBe(false); - - await expect(ensureProjectCacheDir(rootPath)).resolves.toEqual({ - status: 'available', - path: cachePath, - }); - expect(readFileSync(ignorePath, 'utf8')).toBe('*\n'); - - writeFileSync(ignorePath, 'stale\n'); - await ensureProjectCacheDir(rootPath); - expect(readFileSync(ignorePath, 'utf8')).toBe('*\n'); - }); -}); - -test('reports an unavailable project cache without throwing', async () => { - await withTempProject(async (rootPath) => { - writeProjectFile(rootPath, '.rstack', 'not a directory'); - - const result = await ensureProjectCacheDir(rootPath); - - expect(result).toMatchObject({ - status: 'unavailable', - path: getProjectCacheDir(rootPath), - }); - }); -}); diff --git a/packages/rstack/tests/setup/hooks.test.ts b/packages/rstack/tests/setup/hooks.test.ts index 5f2f8f0e..3baa5348 100644 --- a/packages/rstack/tests/setup/hooks.test.ts +++ b/packages/rstack/tests/setup/hooks.test.ts @@ -6,9 +6,7 @@ import { createHookFiles } from '../../src/setup/hooks.ts'; import { withDirectory } from './helpers.ts'; test('generates the runner and all client-side Git hook shims', () => { - const { runner, ...shims } = createHookFiles(); - - expect(Object.keys(shims)).toEqual([ + expect(Object.keys(createHookFiles()).filter((name) => name !== 'runner')).toEqual([ 'pre-commit', 'pre-merge-commit', 'prepare-commit-msg', @@ -24,7 +22,6 @@ test('generates the runner and all client-side Git hook shims', () => { 'pre-push', 'pre-auto-gc', ]); - expect(runner).toBeTruthy(); }); test.runIf(process.platform === 'win32')('converts Windows Node paths', () => { diff --git a/packages/rstack/tests/setup/install.test.ts b/packages/rstack/tests/setup/install.test.ts index c1ca1cd5..355d77c7 100644 --- a/packages/rstack/tests/setup/install.test.ts +++ b/packages/rstack/tests/setup/install.test.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { expect, test } from 'rstack/test'; import { createHookFiles } from '../../src/setup/hooks.ts'; import { installHooks } from '../../src/setup/install.ts'; -import { git, hooksPath, restoreEnv, runGit, withDirectory, withRepository } from './helpers.ts'; +import { git, hooksPath, restoreEnv, runGit, withRepository } from './helpers.ts'; test('installs generated hooks and configures the repository', () => { withRepository((cwd) => { @@ -86,32 +86,6 @@ test('resolves repository context with a single Git process when unchanged', () }); }); -test('skips non-Git directories without creating files', () => { - withDirectory((cwd) => { - expect(installHooks({ cwd })).toEqual({ - status: 'skipped', - reason: 'not-git-repository', - }); - expect(existsSync(path.join(cwd, '.rstack'))).toBe(false); - }); -}); - -test('reports when Git is unavailable', () => { - withDirectory((cwd) => { - const originalPath = process.env.PATH; - process.env.PATH = ''; - try { - expect(installHooks({ cwd })).toEqual({ - status: 'failed', - reason: 'git-not-found', - message: 'Git command not found.', - }); - } finally { - restoreEnv('PATH', originalPath); - } - }); -}); - test('does not configure Git when writing generated files fails', () => { withRepository((cwd) => { writeFileSync(path.join(cwd, '.rstack'), 'not a directory');