Skip to content

Commit

Permalink
feat(config): Implement Zod for robust configuration validation
Browse files Browse the repository at this point in the history
  • Loading branch information
yamadashy committed Nov 10, 2024
1 parent 9726e3a commit e70a1d3
Show file tree
Hide file tree
Showing 24 changed files with 368 additions and 215 deletions.
13 changes: 12 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"p-map": "^7.0.2",
"picocolors": "^1.1.1",
"strip-comments": "^2.0.1",
"tiktoken": "^1.0.17"
"tiktoken": "^1.0.17",
"zod": "^3.23.8"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
Expand Down
2 changes: 1 addition & 1 deletion src/cli/actions/defaultAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
RepomixConfigFile,
RepomixConfigMerged,
RepomixOutputStyle,
} from '../../config/configTypes.js';
} from '../../config/configSchema.js';
import { type PackResult, pack } from '../../core/packager.js';
import { logger } from '../../shared/logger.js';
import { printCompletion, printSecurityCheck, printSummary, printTopFiles } from '../cliPrint.js';
Expand Down
2 changes: 1 addition & 1 deletion src/cli/actions/initAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import * as prompts from '@clack/prompts';
import pc from 'picocolors';
import type { RepomixConfigFile, RepomixOutputStyle } from '../../config/configTypes.js';
import type { RepomixConfigFile, RepomixOutputStyle } from '../../config/configSchema.js';
import { defaultConfig, defaultFilePathMap } from '../../config/defaultConfig.js';
import { getGlobalDirectory } from '../../config/globalDirectory.js';
import { logger } from '../../shared/logger.js';
Expand Down
2 changes: 1 addition & 1 deletion src/cli/cliPrint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'node:path';
import pc from 'picocolors';
import type { RepomixConfigMerged } from '../config/configTypes.js';
import type { RepomixConfigMerged } from '../config/configSchema.js';
import type { SuspiciousFileResult } from '../core/security/securityCheck.js';
import { logger } from '../shared/logger.js';

Expand Down
2 changes: 1 addition & 1 deletion src/cli/cliRun.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import process from 'node:process';
import { type OptionValues, program } from 'commander';
import pc from 'picocolors';
import type { RepomixOutputStyle } from '../config/configTypes.js';
import type { RepomixOutputStyle } from '../config/configSchema.js';
import { getVersion } from '../core/file/packageJsonParse.js';
import { handleError } from '../shared/errorHandle.js';
import { logger } from '../shared/logger.js';
Expand Down
37 changes: 19 additions & 18 deletions src/config/configLoad.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { RepomixError } from '../shared/errorHandle.js';
import { RepomixError, rethrowValidationErrorIfZodError } from '../shared/errorHandle.js';
import { logger } from '../shared/logger.js';
import type { RepomixConfigCli, RepomixConfigFile, RepomixConfigMerged } from './configTypes.js';
import { RepomixConfigValidationError, validateConfig } from './configValidate.js';
import { defaultConfig, defaultFilePathMap } from './defaultConfig.js';
import {
type RepomixConfigCli,
type RepomixConfigFile,
type RepomixConfigMerged,
repomixConfigFileSchema,
repomixConfigMergedSchema,
} from './configSchema.js';
import { defaultConfig } from './defaultConfig.js';
import { getGlobalDirectory } from './globalDirectory.js';

const defaultConfigPath = 'repomix.config.json';
Expand All @@ -25,7 +30,6 @@ export const loadFileConfig = async (rootDir: string, argConfigPath: string | nu

logger.trace('Loading local config from:', fullPath);

// Check local file existence
const isLocalFileExists = await fs
.stat(fullPath)
.then((stats) => stats.isFile())
Expand All @@ -36,7 +40,6 @@ export const loadFileConfig = async (rootDir: string, argConfigPath: string | nu
}

if (useDefaultConfig) {
// Try to load global config
const globalConfigPath = getGlobalConfigPath();
logger.trace('Loading global config from:', globalConfigPath);

Expand All @@ -61,12 +64,9 @@ const loadAndValidateConfig = async (filePath: string): Promise<RepomixConfigFil
try {
const fileContent = await fs.readFile(filePath, 'utf-8');
const config = JSON.parse(fileContent);
validateConfig(config);
return config;
return repomixConfigFileSchema.parse(config);
} catch (error) {
if (error instanceof RepomixConfigValidationError) {
throw new RepomixError(`Invalid configuration in ${filePath}: ${error.message}`);
}
rethrowValidationErrorIfZodError(error, 'Invalid config schema');
if (error instanceof SyntaxError) {
throw new RepomixError(`Invalid JSON in config file ${filePath}: ${error.message}`);
}
Expand All @@ -82,13 +82,7 @@ export const mergeConfigs = (
fileConfig: RepomixConfigFile,
cliConfig: RepomixConfigCli,
): RepomixConfigMerged => {
// If the output file path is not provided in the config file or CLI, use the default file path for the style
if (cliConfig.output?.filePath == null && fileConfig.output?.filePath == null) {
const style = cliConfig.output?.style || fileConfig.output?.style || defaultConfig.output.style;
defaultConfig.output.filePath = defaultFilePathMap[style];
}

return {
const mergedConfig = {
cwd,
output: {
...defaultConfig.output,
Expand All @@ -112,4 +106,11 @@ export const mergeConfigs = (
...cliConfig.security,
},
};

try {
return repomixConfigMergedSchema.parse(mergedConfig);
} catch (error) {
rethrowValidationErrorIfZodError(error, 'Invalid merged config');
throw error;
}
};
81 changes: 81 additions & 0 deletions src/config/configSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { z } from 'zod';
import { RepomixError } from '../shared/errorHandle.js';

export const repomixOutputStyleSchema = z.enum(['plain', 'xml', 'markdown']);

export const repomixConfigBaseSchema = z.object({
output: z
.object({
filePath: z.string().optional(),
style: repomixOutputStyleSchema.optional(),
headerText: z.string().optional(),
instructionFilePath: z.string().optional(),
removeComments: z.boolean().optional(),
removeEmptyLines: z.boolean().optional(),
topFilesLength: z.number().optional(),
showLineNumbers: z.boolean().optional(),
copyToClipboard: z.boolean().optional(),
})
.optional(),
include: z.array(z.string()).optional(),
ignore: z
.object({
useGitignore: z.boolean().optional(),
useDefaultPatterns: z.boolean().optional(),
customPatterns: z.array(z.string()).optional(),
})
.optional(),
security: z
.object({
enableSecurityCheck: z.boolean().optional(),
})
.optional(),
});

export const repomixConfigDefaultSchema = repomixConfigBaseSchema.and(
z.object({
output: z.object({
filePath: z.string(),
style: repomixOutputStyleSchema,
headerText: z.string().optional(),
instructionFilePath: z.string().optional(),
removeComments: z.boolean(),
removeEmptyLines: z.boolean(),
topFilesLength: z.number(),
showLineNumbers: z.boolean(),
copyToClipboard: z.boolean(),
}),
include: z.array(z.string()),
ignore: z.object({
useGitignore: z.boolean(),
useDefaultPatterns: z.boolean(),
customPatterns: z.array(z.string()).optional(),
}),
security: z.object({
enableSecurityCheck: z.boolean(),
}),
}),
);

export const repomixConfigFileSchema = repomixConfigBaseSchema;

export const repomixConfigCliSchema = repomixConfigBaseSchema;

export const repomixConfigMergedSchema = repomixConfigDefaultSchema
.and(repomixConfigFileSchema)
.and(repomixConfigCliSchema)
.and(
z.object({
cwd: z.string(),
}),
);

export type RepomixOutputStyle = z.infer<typeof repomixOutputStyleSchema>;

export type RepomixConfigDefault = z.infer<typeof repomixConfigDefaultSchema>;

export type RepomixConfigFile = z.infer<typeof repomixConfigFileSchema>;

export type RepomixConfigCli = z.infer<typeof repomixConfigCliSchema>;

export type RepomixConfigMerged = z.infer<typeof repomixConfigMergedSchema>;
57 changes: 0 additions & 57 deletions src/config/configTypes.ts

This file was deleted.

72 changes: 0 additions & 72 deletions src/config/configValidate.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/config/defaultConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RepomixConfigDefault, RepomixOutputStyle } from './configTypes.js';
import type { RepomixConfigDefault, RepomixOutputStyle } from './configSchema.js';

export const defaultFilePathMap: Record<RepomixOutputStyle, string> = {
plain: 'repomix-output.txt',
Expand Down
2 changes: 1 addition & 1 deletion src/core/file/fileProcess.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pMap from 'p-map';
import type { RepomixConfigMerged } from '../../config/configTypes.js';
import type { RepomixConfigMerged } from '../../config/configSchema.js';
import { getProcessConcurrency } from '../../shared/processConcurrency.js';
import { getFileManipulator } from './fileManipulate.js';
import type { ProcessedFile, RawFile } from './fileTypes.js';
Expand Down
2 changes: 1 addition & 1 deletion src/core/file/fileSearch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { globby } from 'globby';
import type { RepomixConfigMerged } from '../../config/configTypes.js';
import type { RepomixConfigMerged } from '../../config/configSchema.js';
import { defaultIgnoreList } from '../../config/defaultIgnore.js';
import { logger } from '../../shared/logger.js';
import { sortPaths } from './filePathSort.js';
Expand Down
2 changes: 1 addition & 1 deletion src/core/output/outputGenerate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import Handlebars from 'handlebars';
import type { RepomixConfigMerged } from '../../config/configTypes.js';
import type { RepomixConfigMerged } from '../../config/configSchema.js';
import { RepomixError } from '../../shared/errorHandle.js';
import { generateTreeString } from '../file/fileTreeGenerate.js';
import type { ProcessedFile } from '../file/fileTypes.js';
Expand Down
Loading

0 comments on commit e70a1d3

Please sign in to comment.