0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
CI / Shell Format Check (push) Waiting to run
CI / Check Ruby (3.4) (push) Waiting to run
CI / CI Config (push) Waiting to run
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Blocked by required conditions
CI / Build on Node ${{ matrix.node }} (push) Blocked by required conditions
CI / Style Check (push) Waiting to run
CI / Generate Assets (push) Waiting to run
CI / Check Python (3.14) (push) Waiting to run
CI / Check Python (3.9) (push) Waiting to run
CI / Build Docs (push) Waiting to run
CI / Code Scan Action (push) Waiting to run
CI / Site tests (push) Waiting to run
CI / webui tests (push) Waiting to run
CI / Run Integration Tests (push) Waiting to run
CI / Run Smoke Tests (push) Waiting to run
CI / Go Tests (push) Waiting to run
CI / Share Test (push) Waiting to run
CI / Redteam (Production API) (push) Waiting to run
CI / Redteam (Staging API) (push) Waiting to run
CI / GitHub Actions Lint (push) Waiting to run
CI / Check Ruby (3.0) (push) Waiting to run
release-please / release-please (push) Waiting to run
release-please / build (push) Blocked by required conditions
release-please / publish-npm (push) Blocked by required conditions
release-please / publish-npm-backfill (push) Waiting to run
release-please / docker (push) Blocked by required conditions
release-please / publish-code-scan-action (push) Blocked by required conditions
release-please / attest-code-scan-action (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
158 lines
5.7 KiB
TypeScript
158 lines
5.7 KiB
TypeScript
import { type ZodType, z } from 'zod';
|
|
import { UnifiedConfigSchema } from '../src/types';
|
|
import { TRANSFORM_KEYS } from '../src/util/transform';
|
|
import { StringOrFunctionSchema } from '../src/validators/shared';
|
|
|
|
// NOTE: This script accesses Zod's internal _def property to extract the input schema
|
|
// from pipe/transform wrappers. This is necessary because UnifiedConfigSchema uses
|
|
// .pipe() for transforms, and we need the input schema for JSON Schema generation.
|
|
// This may need updating if Zod's internal structure changes.
|
|
|
|
// Recursively unwrap to get the base schema (unwrapping optional, nullable, pipe, transform)
|
|
function getBaseSchema(schema: ZodType): ZodType {
|
|
const def = schema._def as { type?: string; in?: ZodType; innerType?: ZodType };
|
|
|
|
if (def?.type === 'optional' && def?.innerType) {
|
|
return getBaseSchema(def.innerType);
|
|
}
|
|
if (def?.type === 'nullable' && def?.innerType) {
|
|
return getBaseSchema(def.innerType);
|
|
}
|
|
if (def?.type === 'pipe' && def?.in) {
|
|
return getBaseSchema(def.in);
|
|
}
|
|
if (def?.type === 'transform' && def?.in) {
|
|
return getBaseSchema(def.in);
|
|
}
|
|
|
|
return schema;
|
|
}
|
|
|
|
// UnifiedConfigSchema is wrapped in a .pipe() transform, so generate JSON Schema from its input.
|
|
const innerSchema = getBaseSchema(UnifiedConfigSchema);
|
|
|
|
const transformSchemaKeys: Set<string> = new Set(TRANSFORM_KEYS);
|
|
|
|
/**
|
|
* Strips every key from `target` and reassigns it to `{ type: 'string', description? }`,
|
|
* preserving an optional description so generated docs stay useful. Used by both the
|
|
* `override` hook (for inline `StringOrFunctionSchema` nodes) and the post-pass walker
|
|
* (for nodes Zod rewrites after `override` runs).
|
|
*/
|
|
function rewriteNodeToStringSchema(target: Record<string, unknown>): void {
|
|
const description = target.description;
|
|
for (const key of Object.keys(target)) {
|
|
delete target[key];
|
|
}
|
|
if (typeof description === 'string') {
|
|
target.description = description;
|
|
}
|
|
target.type = 'string';
|
|
}
|
|
|
|
function forceStringTransformSchemas(node: unknown): void {
|
|
if (!node || typeof node !== 'object') {
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(node)) {
|
|
for (const item of node) {
|
|
forceStringTransformSchemas(item);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const schemaObject = node as Record<string, unknown>;
|
|
|
|
for (const [key, value] of Object.entries(schemaObject)) {
|
|
if (
|
|
transformSchemaKeys.has(key) &&
|
|
value &&
|
|
typeof value === 'object' &&
|
|
!Array.isArray(value)
|
|
) {
|
|
rewriteNodeToStringSchema(value as Record<string, unknown>);
|
|
// Recursing into the just-rewritten `{type, description}` stub is pointless
|
|
// and could accidentally re-match if a future rewrite leaves nested junk.
|
|
continue;
|
|
}
|
|
|
|
forceStringTransformSchemas(value);
|
|
}
|
|
}
|
|
|
|
// Options for nested toJSONSchema calls (without reused:'ref' to avoid orphaned definitions)
|
|
const nestedOptions = {
|
|
target: 'draft-07' as const,
|
|
unrepresentable: 'any' as const,
|
|
};
|
|
|
|
// Generate JSON Schema using Zod v4's native toJSONSchema
|
|
// - target: 'draft-07' for compatibility with most tooling
|
|
// - unrepresentable: 'any' converts z.custom() functions to {} (accepts any)
|
|
// - reused: 'ref' extracts duplicate schemas to definitions, reducing size significantly
|
|
// - override: handles nested pipe/transform schemas that Zod doesn't convert automatically
|
|
const schemaContent = z.toJSONSchema(innerSchema, {
|
|
...nestedOptions,
|
|
reused: 'ref' as const,
|
|
// biome-ignore lint/suspicious/noExplicitAny: Zod v4 internal types are not publicly exported
|
|
override: (ctx: any) => {
|
|
const zodSchema = ctx.zodSchema as ZodType;
|
|
const def = zodSchema._def as { type?: string; in?: ZodType; innerType?: ZodType };
|
|
const baseSchema = getBaseSchema(zodSchema);
|
|
|
|
// Config files can only represent string transforms. Preserve runtime support for function
|
|
// transforms in the Zod schema, but keep generated JSON Schema string-only for editor/Ajv use.
|
|
if (baseSchema === StringOrFunctionSchema) {
|
|
rewriteNodeToStringSchema(ctx.jsonSchema as Record<string, unknown>);
|
|
return;
|
|
}
|
|
|
|
// Handle optional/nullable wrapping a pipe/transform
|
|
if ((def?.type === 'optional' || def?.type === 'nullable') && def?.innerType) {
|
|
const innerDef = def.innerType._def as { type?: string };
|
|
if (innerDef?.type === 'pipe' || innerDef?.type === 'transform') {
|
|
if (Object.keys(ctx.jsonSchema).length === 0) {
|
|
const result = z.toJSONSchema(baseSchema, nestedOptions);
|
|
const { $schema: _, ...rest } = result as Record<string, unknown>;
|
|
Object.assign(ctx.jsonSchema, rest);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle pipe/transform directly
|
|
if ((def?.type === 'pipe' || def?.type === 'transform') && def?.in) {
|
|
if (Object.keys(ctx.jsonSchema).length === 0) {
|
|
const result = z.toJSONSchema(baseSchema, nestedOptions);
|
|
const { $schema: _, ...rest } = result as Record<string, unknown>;
|
|
Object.assign(ctx.jsonSchema, rest);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
// Extract the main schema parts
|
|
const {
|
|
$schema: _,
|
|
definitions: zodDefinitions,
|
|
...mainSchema
|
|
} = schemaContent as Record<string, unknown>;
|
|
|
|
// Build final schema with proper structure and metadata
|
|
const jsonSchema = {
|
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
$id: 'https://promptfoo.dev/config-schema.json',
|
|
title: 'Promptfoo Configuration Schema',
|
|
$ref: '#/definitions/PromptfooConfigSchema',
|
|
definitions: {
|
|
PromptfooConfigSchema: mainSchema,
|
|
...(zodDefinitions as Record<string, unknown>),
|
|
},
|
|
};
|
|
|
|
// Zod may rewrite reused StringOrFunctionSchema nodes after `override` runs. Do a final pass to keep
|
|
// transform-like fields string-only in JSON Schema while runtime Zod still accepts functions.
|
|
forceStringTransformSchemas(jsonSchema);
|
|
|
|
console.log(JSON.stringify(jsonSchema, null, 2));
|