Skip to content

Commit

Permalink
Merge pull request #167 from yamadashy/feature/zod
Browse files Browse the repository at this point in the history
feat: Implement Zod for Robust Configuration Validation
  • Loading branch information
yamadashy authored Nov 11, 2024
2 parents 9726e3a + 5d05730 commit 9dbe0d1
Show file tree
Hide file tree
Showing 24 changed files with 410 additions and 249 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
21 changes: 14 additions & 7 deletions src/cli/actions/defaultAction.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import path from 'node:path';
import { loadFileConfig, mergeConfigs } from '../../config/configLoad.js';
import type {
RepomixConfigCli,
RepomixConfigFile,
RepomixConfigMerged,
RepomixOutputStyle,
} from '../../config/configTypes.js';
import {
type RepomixConfigCli,
type RepomixConfigFile,
type RepomixConfigMerged,
type RepomixOutputStyle,
repomixConfigCliSchema,
} from '../../config/configSchema.js';
import { type PackResult, pack } from '../../core/packager.js';
import { rethrowValidationErrorIfZodError } from '../../shared/errorHandle.js';
import { logger } from '../../shared/logger.js';
import { printCompletion, printSecurityCheck, printSummary, printTopFiles } from '../cliPrint.js';
import type { CliOptions } from '../cliRun.js';
Expand Down Expand Up @@ -111,5 +113,10 @@ const buildCliConfig = (options: CliOptions): RepomixConfigCli => {
cliConfig.output = { ...cliConfig.output, style: options.style.toLowerCase() as RepomixOutputStyle };
}

return cliConfig;
try {
return repomixConfigCliSchema.parse(cliConfig);
} catch (error) {
rethrowValidationErrorIfZodError(error, 'Invalid cli arguments');
throw error;
}
};
8 changes: 6 additions & 2 deletions src/cli/actions/initAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ 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 { defaultConfig, defaultFilePathMap } from '../../config/defaultConfig.js';
import {
type RepomixConfigFile,
type RepomixOutputStyle,
defaultConfig,
defaultFilePathMap,
} from '../../config/configSchema.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
51 changes: 34 additions & 17 deletions src/config/configLoad.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { RepomixError } from '../shared/errorHandle.js';
import { z } from 'zod';
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,
defaultConfig,
defaultFilePathMap,
repomixConfigFileSchema,
repomixConfigMergedSchema,
} from './configSchema.js';
import { getGlobalDirectory } from './globalDirectory.js';

const defaultConfigPath = 'repomix.config.json';
Expand Down Expand Up @@ -61,12 +68,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,34 +86,47 @@ export const mergeConfigs = (
fileConfig: RepomixConfigFile,
cliConfig: RepomixConfigCli,
): RepomixConfigMerged => {
logger.trace('Default config:', defaultConfig);

const baseConfig = defaultConfig;

// 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];
const style = cliConfig.output?.style || fileConfig.output?.style || baseConfig.output.style;
baseConfig.output.filePath = defaultFilePathMap[style];

logger.trace('Default output file path is set to:', baseConfig.output.filePath);
}

return {
const mergedConfig = {
cwd,
output: {
...defaultConfig.output,
...baseConfig.output,
...fileConfig.output,
...cliConfig.output,
},
include: [...(baseConfig.include || []), ...(fileConfig.include || []), ...(cliConfig.include || [])],
ignore: {
...defaultConfig.ignore,
...baseConfig.ignore,
...fileConfig.ignore,
...cliConfig.ignore,
customPatterns: [
...(defaultConfig.ignore.customPatterns || []),
...(baseConfig.ignore.customPatterns || []),
...(fileConfig.ignore?.customPatterns || []),
...(cliConfig.ignore?.customPatterns || []),
],
},
include: [...(defaultConfig.include || []), ...(fileConfig.include || []), ...(cliConfig.include || [])],
security: {
...defaultConfig.security,
...baseConfig.security,
...fileConfig.security,
...cliConfig.security,
},
};

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

// Output style enum
export const repomixOutputStyleSchema = z.enum(['plain', 'xml', 'markdown']);
export type RepomixOutputStyle = z.infer<typeof repomixOutputStyleSchema>;

// Default values map
export const defaultFilePathMap: Record<RepomixOutputStyle, string> = {
plain: 'repomix-output.txt',
markdown: 'repomix-output.md',
xml: 'repomix-output.xml',
} as const;

// Base config schema
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(),
});

// Default config schema with default values
export const repomixConfigDefaultSchema = z.object({
output: z
.object({
filePath: z.string().default(defaultFilePathMap.plain),
style: repomixOutputStyleSchema.default('plain'),
headerText: z.string().optional(),
instructionFilePath: z.string().optional(),
removeComments: z.boolean().default(false),
removeEmptyLines: z.boolean().default(false),
topFilesLength: z.number().int().min(0).default(5),
showLineNumbers: z.boolean().default(false),
copyToClipboard: z.boolean().default(false),
})
.default({}),
include: z.array(z.string()).default([]),
ignore: z
.object({
useGitignore: z.boolean().default(true),
useDefaultPatterns: z.boolean().default(true),
customPatterns: z.array(z.string()).default([]),
})
.default({}),
security: z
.object({
enableSecurityCheck: z.boolean().default(true),
})
.default({}),
});

export const repomixConfigFileSchema = repomixConfigBaseSchema;

export const repomixConfigCliSchema = repomixConfigBaseSchema;

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

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>;

export const defaultConfig = repomixConfigDefaultSchema.parse({});
57 changes: 0 additions & 57 deletions src/config/configTypes.ts

This file was deleted.

Loading

0 comments on commit 9dbe0d1

Please sign in to comment.