diff --git a/src/config/configLoad.ts b/src/config/configLoad.ts index 621201de4..7b0187980 100644 --- a/src/config/configLoad.ts +++ b/src/config/configLoad.ts @@ -15,56 +15,72 @@ import { } from './configSchema.js'; import { getGlobalDirectory } from './globalDirectory.js'; -const defaultConfigPath = 'repomix.config.json'; +const defaultConfigPaths = ['repomix.config.json5', 'repomix.config.jsonc', 'repomix.config.json']; -const getGlobalConfigPath = () => { - return path.join(getGlobalDirectory(), 'repomix.config.json'); +const getGlobalConfigPaths = () => { + const globalDir = getGlobalDirectory(); + return defaultConfigPaths.map((configPath) => path.join(globalDir, configPath)); }; -export const loadFileConfig = async (rootDir: string, argConfigPath: string | null): Promise => { - let useDefaultConfig = false; - let configPath = argConfigPath; - if (!configPath) { - useDefaultConfig = true; - configPath = defaultConfigPath; +const checkFileExists = async (filePath: string): Promise => { + try { + const stats = await fs.stat(filePath); + return stats.isFile(); + } catch { + return false; } +}; - const fullPath = path.resolve(rootDir, configPath); - - logger.trace('Loading local config from:', fullPath); +const findConfigFile = async (configPaths: string[], logPrefix: string): Promise => { + for (const configPath of configPaths) { + logger.trace(`Checking for ${logPrefix} config at:`, configPath); - // Check local file existence - const isLocalFileExists = await fs - .stat(fullPath) - .then((stats) => stats.isFile()) - .catch(() => false); + const fileExists = await checkFileExists(configPath); - if (isLocalFileExists) { - return await loadAndValidateConfig(fullPath); + if (fileExists) { + logger.trace(`Found ${logPrefix} config at:`, configPath); + return configPath; + } } + return null; +}; - if (useDefaultConfig) { - // Try to load global config - const globalConfigPath = getGlobalConfigPath(); - logger.trace('Loading global config from:', globalConfigPath); +export const loadFileConfig = async (rootDir: string, argConfigPath: string | null): Promise => { + if (argConfigPath) { + // If a specific config path is provided, use it directly + const fullPath = path.resolve(rootDir, argConfigPath); + logger.trace('Loading local config from:', fullPath); - const isGlobalFileExists = await fs - .stat(globalConfigPath) - .then((stats) => stats.isFile()) - .catch(() => false); + const isLocalFileExists = await checkFileExists(fullPath); - if (isGlobalFileExists) { - return await loadAndValidateConfig(globalConfigPath); + if (isLocalFileExists) { + return await loadAndValidateConfig(fullPath); } + throw new RepomixError(`Config file not found at ${argConfigPath}`); + } + + // Try to find a local config file using the priority order + const localConfigPaths = defaultConfigPaths.map((configPath) => path.resolve(rootDir, configPath)); + const localConfigPath = await findConfigFile(localConfigPaths, 'local'); + + if (localConfigPath) { + return await loadAndValidateConfig(localConfigPath); + } + + // Try to find a global config file using the priority order + const globalConfigPaths = getGlobalConfigPaths(); + const globalConfigPath = await findConfigFile(globalConfigPaths, 'global'); - logger.log( - pc.dim( - `No custom config found at ${configPath} or global config at ${globalConfigPath}.\nYou can add a config file for additional settings. Please check https://github.com/yamadashy/repomix for more information.`, - ), - ); - return {}; + if (globalConfigPath) { + return await loadAndValidateConfig(globalConfigPath); } - throw new RepomixError(`Config file not found at ${configPath}`); + + logger.log( + pc.dim( + `No custom config found at ${defaultConfigPaths.join(', ')} or global config at ${globalConfigPaths.join(', ')}.\nYou can add a config file for additional settings. Please check https://github.com/yamadashy/repomix for more information.`, + ), + ); + return {}; }; const loadAndValidateConfig = async (filePath: string): Promise => { diff --git a/tests/config/configLoad.test.ts b/tests/config/configLoad.test.ts index 765d2f495..7df7aba89 100644 --- a/tests/config/configLoad.test.ts +++ b/tests/config/configLoad.test.ts @@ -7,7 +7,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { loadFileConfig, mergeConfigs } from '../../src/config/configLoad.js'; import type { RepomixConfigCli, RepomixConfigFile } from '../../src/config/configSchema.js'; import { getGlobalDirectory } from '../../src/config/globalDirectory.js'; -import { RepomixConfigValidationError } from '../../src/shared/errorHandle.js'; +import { RepomixConfigValidationError, RepomixError } from '../../src/shared/errorHandle.js'; import { logger } from '../../src/shared/logger.js'; vi.mock('node:fs/promises'); @@ -59,13 +59,15 @@ describe('configLoad', () => { }; vi.mocked(getGlobalDirectory).mockReturnValue('/global/repomix'); vi.mocked(fs.stat) - .mockRejectedValueOnce(new Error('File not found')) // Local config - .mockResolvedValueOnce({ isFile: () => true } as Stats); // Global config + .mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.json5 + .mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.jsonc + .mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.json + .mockResolvedValueOnce({ isFile: () => true } as Stats); // Global repomix.config.json5 vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockGlobalConfig)); const result = await loadFileConfig(process.cwd(), null); expect(result).toEqual(mockGlobalConfig); - expect(fs.readFile).toHaveBeenCalledWith(path.join('/global/repomix', 'repomix.config.json'), 'utf-8'); + expect(fs.readFile).toHaveBeenCalledWith(path.join('/global/repomix', 'repomix.config.json5'), 'utf-8'); }); test('should return an empty object if no config file is found', async () => { @@ -77,6 +79,9 @@ describe('configLoad', () => { expect(result).toEqual({}); expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('No custom config found')); + expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('repomix.config.json5')); + expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('repomix.config.jsonc')); + expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('repomix.config.json')); }); test('should throw an error for invalid JSON', async () => { @@ -138,6 +143,45 @@ describe('configLoad', () => { }, }); }); + + test('should load .jsonc config file with priority order', async () => { + const mockConfig = { + output: { filePath: 'jsonc-output.txt' }, + ignore: { useDefaultPatterns: true }, + }; + vi.mocked(fs.stat) + .mockRejectedValueOnce(new Error('File not found')) // repomix.config.json5 + .mockResolvedValueOnce({ isFile: () => true } as Stats); // repomix.config.jsonc + vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig)); + + const result = await loadFileConfig(process.cwd(), null); + expect(result).toEqual(mockConfig); + expect(fs.readFile).toHaveBeenCalledWith(path.resolve(process.cwd(), 'repomix.config.jsonc'), 'utf-8'); + }); + + test('should prioritize .json5 over .jsonc and .json', async () => { + const mockConfig = { + output: { filePath: 'json5-output.txt' }, + ignore: { useDefaultPatterns: true }, + }; + vi.mocked(fs.stat).mockResolvedValueOnce({ isFile: () => true } as Stats); // repomix.config.json5 exists + vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig)); + + const result = await loadFileConfig(process.cwd(), null); + expect(result).toEqual(mockConfig); + expect(fs.readFile).toHaveBeenCalledWith(path.resolve(process.cwd(), 'repomix.config.json5'), 'utf-8'); + // Should not check for .jsonc or .json since .json5 was found + expect(fs.stat).toHaveBeenCalledTimes(1); + }); + + test('should throw RepomixError when specific config file does not exist', async () => { + const nonExistentConfigPath = 'non-existent-config.json'; + vi.mocked(fs.stat).mockRejectedValue(new Error('File not found')); + + await expect(loadFileConfig(process.cwd(), nonExistentConfigPath)).rejects.toThrow( + `Config file not found at ${nonExistentConfigPath}`, + ); + }); }); describe('mergeConfigs', () => {