+
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
119 changes: 119 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,123 @@ describe('index', () => {

expect(() => processConfig(config, ['unknown'])).toThrow(/Unknown command:.*unknown/);
});

it('should throw error for unknown long option', () => {
const config = defineConfig({
commands: {
test: defineCommand({
options: defineOptions(
z.object({
verbose: z.boolean().default(false),
}),
),
action: vi.fn(),
}),
},
});

expect(() => processConfig(config, ['test', '--unknown'])).toThrow(ZliError);
expect(() => processConfig(config, ['test', '--unknown'])).toThrow('Unknown option: \x1b[36m--unknown\x1b[0m');
});

it('should throw error for unknown short option', () => {
const config = defineConfig({
commands: {
test: defineCommand({
options: defineOptions(
z.object({
verbose: z.boolean().default(false),
}),
{ v: 'verbose' },
),
action: vi.fn(),
}),
},
});

expect(() => processConfig(config, ['test', '-x'])).toThrow(ZliError);
expect(() => processConfig(config, ['test', '-x'])).toThrow('Unknown option: \x1b[36m-x\x1b[0m');
});

it('should throw error for unknown option with kebab-case display', () => {
const config = defineConfig({
commands: {
test: defineCommand({
options: defineOptions(
z.object({
verbose: z.boolean().default(false),
}),
),
action: vi.fn(),
}),
},
});

expect(() => processConfig(config, ['test', '--no-build'])).toThrow(ZliError);
expect(() => processConfig(config, ['test', '--no-build'])).toThrow('Unknown option: \x1b[36m--no-build\x1b[0m');
});

it('should allow valid kebab-case options', () => {
const config = defineConfig({
commands: {
test: defineCommand({
options: defineOptions(
z.object({
androidMax: z.string(),
verbose: z.boolean().default(false),
}),
),
action: vi.fn(),
}),
},
});

const result = processConfig(config, ['test', '--android-max', '10', '--verbose']);
expect(result.options).toEqual({ androidMax: '10', verbose: true });
});

it('should allow standard help and version flags', () => {
const config = defineConfig({
commands: {
test: defineCommand({
options: defineOptions(
z.object({
name: z.string(),
}),
),
action: vi.fn(),
}),
},
});

// Help should trigger exit, not unknown option error
expect(() => processConfig(config, ['test', '--help'])).toThrow('process.exit called');
});

it('should throw error for unknown option when no schema defined', () => {
const config = defineConfig({
commands: {
test: defineCommand({
action: vi.fn(),
}),
},
});

expect(() => processConfig(config, ['test', '--unknown'])).toThrow(ZliError);
expect(() => processConfig(config, ['test', '--unknown'])).toThrow('Unknown option: \x1b[36m--unknown\x1b[0m');
});

it('should allow help and version when no schema defined', () => {
const config = defineConfig({
meta: { version: '1.0.0' },
commands: {
test: defineCommand({
action: vi.fn(),
}),
},
});

// Help should trigger exit, not unknown option error
expect(() => processConfig(config, ['test', '--help'])).toThrow('process.exit called');
});
});
56 changes: 53 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,18 @@ function isZodArrayType(zodType: any): boolean {
return innerType instanceof z.ZodArray || (innerType._def && innerType._def.typeName === 'ZodArray');
}

/**
* Creates an error for unknown options with proper flag prefix and display name.
*
* @param optionName - The unknown option name
* @returns ZliError with formatted error message
*/
function createUnknownOptionError(optionName: string): ZliError {
const flagPrefix = optionName.length === 1 ? '-' : '--';
const displayName = optionName.length === 1 ? optionName : camelToKebab(optionName);
return new ZliError(`Unknown option: \x1b[36m${flagPrefix}${displayName}\x1b[0m`);
}

/**
* Extracts the default value from a Zod type, handling nested optional and default wrappers.
*
Expand Down Expand Up @@ -390,11 +402,12 @@ function extractDefaultValue(zodType: any): string | undefined {
* Validates and transforms command options using Zod schema validation.
* Processes aliases and kebab-case conversion before validation.
* Ensures that single values for array fields are converted to arrays.
* Throws an error if any unknown options are provided.
*
* @param flags - Raw parsed flags from command line
* @param optionsDef - Optional options definition with schema and aliases
* @returns Validated options object matching the schema
* @throws Error if validation fails
* @throws Error if validation fails or unknown options are provided
*
* @example
* validateOptions({ 'android-max': '10' }, { schema: z.object({ androidMax: z.string() }) })
Expand All @@ -409,15 +422,52 @@ function validateOptions<T extends z.ZodObject<any> = z.ZodObject<any>>(
optionsDef?: OptionsDefinition<T>,
): any {
if (!optionsDef) {
// Check for unknown options when no schema is defined
const { _, ...options } = flags;
const unknownOptions = Object.keys(options);
if (unknownOptions.length > 0) {
// Find the first unknown option that looks like a flag
const firstUnknown = unknownOptions.find((key) => key !== 'help' && key !== 'version');
if (firstUnknown) {
throw createUnknownOptionError(firstUnknown);
}
}
return {};
}

// Get valid option names from schema
const schemaKeys = Object.keys(optionsDef.schema.shape);
const validNames = new Set<string>();

// Add camelCase names
schemaKeys.forEach((key) => validNames.add(key));

// Add kebab-case versions
schemaKeys.forEach((key) => validNames.add(camelToKebab(key)));

// Add aliases
if (optionsDef.aliases) {
Object.keys(optionsDef.aliases).forEach((alias) => validNames.add(alias));
}

// Add standard help and version flags
validNames.add('help');
validNames.add('version');

// Check for unknown options before processing
const { _, ...options } = flags;
for (const optionName of Object.keys(options)) {
if (!validNames.has(optionName)) {
throw createUnknownOptionError(optionName);
}
}

const resolvedAliases = resolveAliases(flags, optionsDef.aliases);
const resolvedKebab = resolveKebabCase(resolvedAliases, optionsDef.schema);
const { _, ...options } = resolvedKebab;
const { _: resolvedUnderscore, ...resolvedOptions } = resolvedKebab;

// Normalize single values to arrays for fields that expect arrays
const normalizedOptions = normalizeArrayFields(options, optionsDef.schema);
const normalizedOptions = normalizeArrayFields(resolvedOptions, optionsDef.schema);

return optionsDef.schema.parse(normalizedOptions);
}
Expand Down
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载