Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 52 additions & 36 deletions src/config/configLoad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RepomixConfigFile> => {
let useDefaultConfig = false;
let configPath = argConfigPath;
if (!configPath) {
useDefaultConfig = true;
configPath = defaultConfigPath;
const checkFileExists = async (filePath: string): Promise<boolean> => {
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<string | null> => {
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<RepomixConfigFile> => {
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<RepomixConfigFile> => {
Expand Down
52 changes: 48 additions & 4 deletions tests/config/configLoad.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading