Skip to content

Commit 66f714d

Browse files
authored
Merge pull request #620 from yamadashy/claude/issue-618-20250531_114912
feat(config): Add support for .jsonc extension with priority order
2 parents 6f5bb2e + 80e1bef commit 66f714d

File tree

2 files changed

+100
-40
lines changed

2 files changed

+100
-40
lines changed

src/config/configLoad.ts

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,56 +15,72 @@ import {
1515
} from './configSchema.js';
1616
import { getGlobalDirectory } from './globalDirectory.js';
1717

18-
const defaultConfigPath = 'repomix.config.json';
18+
const defaultConfigPaths = ['repomix.config.json5', 'repomix.config.jsonc', 'repomix.config.json'];
1919

20-
const getGlobalConfigPath = () => {
21-
return path.join(getGlobalDirectory(), 'repomix.config.json');
20+
const getGlobalConfigPaths = () => {
21+
const globalDir = getGlobalDirectory();
22+
return defaultConfigPaths.map((configPath) => path.join(globalDir, configPath));
2223
};
2324

24-
export const loadFileConfig = async (rootDir: string, argConfigPath: string | null): Promise<RepomixConfigFile> => {
25-
let useDefaultConfig = false;
26-
let configPath = argConfigPath;
27-
if (!configPath) {
28-
useDefaultConfig = true;
29-
configPath = defaultConfigPath;
25+
const checkFileExists = async (filePath: string): Promise<boolean> => {
26+
try {
27+
const stats = await fs.stat(filePath);
28+
return stats.isFile();
29+
} catch {
30+
return false;
3031
}
32+
};
3133

32-
const fullPath = path.resolve(rootDir, configPath);
33-
34-
logger.trace('Loading local config from:', fullPath);
34+
const findConfigFile = async (configPaths: string[], logPrefix: string): Promise<string | null> => {
35+
for (const configPath of configPaths) {
36+
logger.trace(`Checking for ${logPrefix} config at:`, configPath);
3537

36-
// Check local file existence
37-
const isLocalFileExists = await fs
38-
.stat(fullPath)
39-
.then((stats) => stats.isFile())
40-
.catch(() => false);
38+
const fileExists = await checkFileExists(configPath);
4139

42-
if (isLocalFileExists) {
43-
return await loadAndValidateConfig(fullPath);
40+
if (fileExists) {
41+
logger.trace(`Found ${logPrefix} config at:`, configPath);
42+
return configPath;
43+
}
4444
}
45+
return null;
46+
};
4547

46-
if (useDefaultConfig) {
47-
// Try to load global config
48-
const globalConfigPath = getGlobalConfigPath();
49-
logger.trace('Loading global config from:', globalConfigPath);
48+
export const loadFileConfig = async (rootDir: string, argConfigPath: string | null): Promise<RepomixConfigFile> => {
49+
if (argConfigPath) {
50+
// If a specific config path is provided, use it directly
51+
const fullPath = path.resolve(rootDir, argConfigPath);
52+
logger.trace('Loading local config from:', fullPath);
5053

51-
const isGlobalFileExists = await fs
52-
.stat(globalConfigPath)
53-
.then((stats) => stats.isFile())
54-
.catch(() => false);
54+
const isLocalFileExists = await checkFileExists(fullPath);
5555

56-
if (isGlobalFileExists) {
57-
return await loadAndValidateConfig(globalConfigPath);
56+
if (isLocalFileExists) {
57+
return await loadAndValidateConfig(fullPath);
5858
}
59+
throw new RepomixError(`Config file not found at ${argConfigPath}`);
60+
}
61+
62+
// Try to find a local config file using the priority order
63+
const localConfigPaths = defaultConfigPaths.map((configPath) => path.resolve(rootDir, configPath));
64+
const localConfigPath = await findConfigFile(localConfigPaths, 'local');
65+
66+
if (localConfigPath) {
67+
return await loadAndValidateConfig(localConfigPath);
68+
}
69+
70+
// Try to find a global config file using the priority order
71+
const globalConfigPaths = getGlobalConfigPaths();
72+
const globalConfigPath = await findConfigFile(globalConfigPaths, 'global');
5973

60-
logger.log(
61-
pc.dim(
62-
`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.`,
63-
),
64-
);
65-
return {};
74+
if (globalConfigPath) {
75+
return await loadAndValidateConfig(globalConfigPath);
6676
}
67-
throw new RepomixError(`Config file not found at ${configPath}`);
77+
78+
logger.log(
79+
pc.dim(
80+
`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.`,
81+
),
82+
);
83+
return {};
6884
};
6985

7086
const loadAndValidateConfig = async (filePath: string): Promise<RepomixConfigFile> => {

tests/config/configLoad.test.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
77
import { loadFileConfig, mergeConfigs } from '../../src/config/configLoad.js';
88
import type { RepomixConfigCli, RepomixConfigFile } from '../../src/config/configSchema.js';
99
import { getGlobalDirectory } from '../../src/config/globalDirectory.js';
10-
import { RepomixConfigValidationError } from '../../src/shared/errorHandle.js';
10+
import { RepomixConfigValidationError, RepomixError } from '../../src/shared/errorHandle.js';
1111
import { logger } from '../../src/shared/logger.js';
1212

1313
vi.mock('node:fs/promises');
@@ -59,13 +59,15 @@ describe('configLoad', () => {
5959
};
6060
vi.mocked(getGlobalDirectory).mockReturnValue('/global/repomix');
6161
vi.mocked(fs.stat)
62-
.mockRejectedValueOnce(new Error('File not found')) // Local config
63-
.mockResolvedValueOnce({ isFile: () => true } as Stats); // Global config
62+
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.json5
63+
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.jsonc
64+
.mockRejectedValueOnce(new Error('File not found')) // Local repomix.config.json
65+
.mockResolvedValueOnce({ isFile: () => true } as Stats); // Global repomix.config.json5
6466
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockGlobalConfig));
6567

6668
const result = await loadFileConfig(process.cwd(), null);
6769
expect(result).toEqual(mockGlobalConfig);
68-
expect(fs.readFile).toHaveBeenCalledWith(path.join('/global/repomix', 'repomix.config.json'), 'utf-8');
70+
expect(fs.readFile).toHaveBeenCalledWith(path.join('/global/repomix', 'repomix.config.json5'), 'utf-8');
6971
});
7072

7173
test('should return an empty object if no config file is found', async () => {
@@ -77,6 +79,9 @@ describe('configLoad', () => {
7779
expect(result).toEqual({});
7880

7981
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('No custom config found'));
82+
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('repomix.config.json5'));
83+
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('repomix.config.jsonc'));
84+
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('repomix.config.json'));
8085
});
8186

8287
test('should throw an error for invalid JSON', async () => {
@@ -138,6 +143,45 @@ describe('configLoad', () => {
138143
},
139144
});
140145
});
146+
147+
test('should load .jsonc config file with priority order', async () => {
148+
const mockConfig = {
149+
output: { filePath: 'jsonc-output.txt' },
150+
ignore: { useDefaultPatterns: true },
151+
};
152+
vi.mocked(fs.stat)
153+
.mockRejectedValueOnce(new Error('File not found')) // repomix.config.json5
154+
.mockResolvedValueOnce({ isFile: () => true } as Stats); // repomix.config.jsonc
155+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
156+
157+
const result = await loadFileConfig(process.cwd(), null);
158+
expect(result).toEqual(mockConfig);
159+
expect(fs.readFile).toHaveBeenCalledWith(path.resolve(process.cwd(), 'repomix.config.jsonc'), 'utf-8');
160+
});
161+
162+
test('should prioritize .json5 over .jsonc and .json', async () => {
163+
const mockConfig = {
164+
output: { filePath: 'json5-output.txt' },
165+
ignore: { useDefaultPatterns: true },
166+
};
167+
vi.mocked(fs.stat).mockResolvedValueOnce({ isFile: () => true } as Stats); // repomix.config.json5 exists
168+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
169+
170+
const result = await loadFileConfig(process.cwd(), null);
171+
expect(result).toEqual(mockConfig);
172+
expect(fs.readFile).toHaveBeenCalledWith(path.resolve(process.cwd(), 'repomix.config.json5'), 'utf-8');
173+
// Should not check for .jsonc or .json since .json5 was found
174+
expect(fs.stat).toHaveBeenCalledTimes(1);
175+
});
176+
177+
test('should throw RepomixError when specific config file does not exist', async () => {
178+
const nonExistentConfigPath = 'non-existent-config.json';
179+
vi.mocked(fs.stat).mockRejectedValue(new Error('File not found'));
180+
181+
await expect(loadFileConfig(process.cwd(), nonExistentConfigPath)).rejects.toThrow(
182+
`Config file not found at ${nonExistentConfigPath}`,
183+
);
184+
});
141185
});
142186

143187
describe('mergeConfigs', () => {

0 commit comments

Comments
 (0)