Skip to main content
Glama
configLoad.ts7.56 kB
import * as fs from 'node:fs/promises'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { createJiti } from 'jiti'; import JSON5 from 'json5'; import pc from 'picocolors'; import { RepomixError, rethrowValidationErrorIfZodError } from '../shared/errorHandle.js'; import { logger } from '../shared/logger.js'; import { defaultConfig, defaultFilePathMap, type RepomixConfigCli, type RepomixConfigFile, type RepomixConfigMerged, repomixConfigFileSchema, repomixConfigMergedSchema, } from './configSchema.js'; import { getGlobalDirectory } from './globalDirectory.js'; const defaultConfigPaths = [ 'repomix.config.ts', 'repomix.config.mts', 'repomix.config.cts', 'repomix.config.js', 'repomix.config.mjs', 'repomix.config.cjs', 'repomix.config.json5', 'repomix.config.jsonc', 'repomix.config.json', ]; const getGlobalConfigPaths = () => { const globalDir = getGlobalDirectory(); return defaultConfigPaths.map((configPath) => path.join(globalDir, configPath)); }; const checkFileExists = async (filePath: string): Promise<boolean> => { try { const stats = await fs.stat(filePath); return stats.isFile(); } catch { return false; } }; const findConfigFile = async (configPaths: string[], logPrefix: string): Promise<string | null> => { for (const configPath of configPaths) { logger.trace(`Checking for ${logPrefix} config at:`, configPath); const fileExists = await checkFileExists(configPath); if (fileExists) { logger.trace(`Found ${logPrefix} config at:`, configPath); return configPath; } } return null; }; // Default jiti import implementation for loading JS/TS config files const defaultJitiImport = async (fileUrl: string): Promise<unknown> => { const jiti = createJiti(import.meta.url, { moduleCache: false, // Disable cache to ensure fresh config loads interopDefault: true, // Automatically use default export }); return await jiti.import(fileUrl); }; export const loadFileConfig = async ( rootDir: string, argConfigPath: string | null, deps = { jitiImport: defaultJitiImport, }, ): 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 isLocalFileExists = await checkFileExists(fullPath); if (isLocalFileExists) { return await loadAndValidateConfig(fullPath, deps); } 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, deps); } // Try to find a global config file using the priority order const globalConfigPaths = getGlobalConfigPaths(); const globalConfigPath = await findConfigFile(globalConfigPaths, 'global'); if (globalConfigPath) { return await loadAndValidateConfig(globalConfigPath, deps); } 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 getFileExtension = (filePath: string): string => { const match = filePath.match(/\.(ts|mts|cts|js|mjs|cjs|json5|jsonc|json)$/); return match ? match[1] : ''; }; // Dependency injection allows mocking jiti in tests to prevent double instrumentation. // Without this, jiti transforms src/ files that are already instrumented by Vitest, // causing coverage instability (results varied by ~2% on each test run). const loadAndValidateConfig = async ( filePath: string, deps = { jitiImport: defaultJitiImport, }, ): Promise<RepomixConfigFile> => { try { let config: unknown; const ext = getFileExtension(filePath); switch (ext) { case 'ts': case 'mts': case 'cts': case 'js': case 'mjs': case 'cjs': { // Use jiti for TypeScript and JavaScript files // This provides consistent behavior and avoids Node.js module cache issues config = await deps.jitiImport(pathToFileURL(filePath).href); break; } case 'json5': case 'jsonc': case 'json': { // Use JSON5 for JSON/JSON5/JSONC files const fileContent = await fs.readFile(filePath, 'utf-8'); config = JSON5.parse(fileContent); break; } default: throw new RepomixError(`Unsupported config file format: ${filePath}`); } return repomixConfigFileSchema.parse(config); } catch (error) { rethrowValidationErrorIfZodError(error, 'Invalid config schema'); if (error instanceof SyntaxError) { throw new RepomixError(`Invalid syntax in config file ${filePath}: ${error.message}`); } if (error instanceof Error) { throw new RepomixError(`Error loading config from ${filePath}: ${error.message}`); } throw new RepomixError(`Error loading config from ${filePath}`); } }; export const mergeConfigs = ( cwd: string, fileConfig: RepomixConfigFile, cliConfig: RepomixConfigCli, ): RepomixConfigMerged => { logger.trace('Default config:', defaultConfig); const baseConfig = defaultConfig; const mergedConfig = { cwd, input: { ...baseConfig.input, ...fileConfig.input, ...cliConfig.input, }, output: (() => { const mergedOutput = { ...baseConfig.output, ...fileConfig.output, ...cliConfig.output, git: { ...baseConfig.output.git, ...fileConfig.output?.git, ...cliConfig.output?.git, }, }; // Auto-adjust filePath extension to match style when filePath is not explicitly set const style = mergedOutput.style ?? baseConfig.output.style; const filePathExplicitlySet = Boolean(fileConfig.output?.filePath || cliConfig.output?.filePath); if (!filePathExplicitlySet) { const desiredPath = defaultFilePathMap[style]; if (mergedOutput.filePath !== desiredPath) { mergedOutput.filePath = desiredPath; logger.trace('Adjusted output file path to match style:', mergedOutput.filePath); } } return mergedOutput; })(), include: [...(baseConfig.include || []), ...(fileConfig.include || []), ...(cliConfig.include || [])], ignore: { ...baseConfig.ignore, ...fileConfig.ignore, ...cliConfig.ignore, customPatterns: [ ...(baseConfig.ignore.customPatterns || []), ...(fileConfig.ignore?.customPatterns || []), ...(cliConfig.ignore?.customPatterns || []), ], }, security: { ...baseConfig.security, ...fileConfig.security, ...cliConfig.security, }, tokenCount: { ...baseConfig.tokenCount, ...fileConfig.tokenCount, ...cliConfig.tokenCount, }, // Skill generation (CLI only) ...(cliConfig.skillGenerate !== undefined && { skillGenerate: cliConfig.skillGenerate }), }; try { return repomixConfigMergedSchema.parse(mergedConfig); } catch (error) { rethrowValidationErrorIfZodError(error, 'Invalid merged config'); throw error; } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yamadashy/repomix'

If you have feedback or need assistance with the MCP directory API, please join our Discord server