Skip to content

Commit 635394e

Browse files
Code-Hexclaude
andauthored
refactor: strengthen test infrastructure and consolidate shared logic (#1427)
* refactor: strengthen test infrastructure and consolidate shared logic - Add tests/helpers/schemas.ts with common GraphQL schema constants shared across all five validator spec files (PrimitiveInput, ArrayInput, etc.) - Add tests/helpers/plugin.ts with a runPlugin() wrapper to reduce buildSchema + plugin() boilerplate in tests - Add expectTypeScriptToCompile checks to myzod, valibot, and yup primitive tests (myzod/valibot had zero compile checks; this adds a baseline) - Extract src/zod/shared.ts: 22 module-level functions (≈250 lines) that were 100% identical in zod/index.ts and zodv4/index.ts; both files now import from the shared module (similarity-ts reported 100% match on generateFieldTypeZodSchema and defaultValueExpression among others) - Add src/lazy.ts with buildMaybeLazy() — the core isComplex check that drives lazy-reference wrapping was duplicated across all five validators; each now passes its own library prefix as a callback - Add src/scalar.ts with buildScalarSchema() — the scalarSchemas → typeMap → defaultScalarTypeSchema → fallback pattern was ~84% similar across all five validators; yup's .defined() wrapping is handled via the wrapCustom option All 357 tests pass unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(scalar): guard against null tsType before 'in' operator check Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(scripts): add --ignoreConfig to example type-check scripts for TypeScript 6 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Apply auto lint-fix changes --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3f80080 commit 635394e

14 files changed

Lines changed: 554 additions & 668 deletions

File tree

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@
5858
],
5959
"scripts": {
6060
"type-check": "tsc --noEmit",
61-
"type-check:yup": "tsc --strict --skipLibCheck --noEmit example/yup/schemas.ts",
62-
"type-check:zod": "tsc --strict --skipLibCheck --noEmit example/zod/schemas.ts",
63-
"type-check:zodv4": "tsc --strict --skipLibCheck --noEmit example/zodv4/schemas.ts",
64-
"type-check:myzod": "tsc --strict --skipLibCheck --noEmit example/myzod/schemas.ts",
65-
"type-check:valibot": "tsc --strict --skipLibCheck --noEmit example/valibot/schemas.ts",
61+
"type-check:yup": "tsc --strict --skipLibCheck --noEmit --ignoreConfig example/yup/schemas.ts",
62+
"type-check:zod": "tsc --strict --skipLibCheck --noEmit --ignoreConfig example/zod/schemas.ts",
63+
"type-check:zodv4": "tsc --strict --skipLibCheck --noEmit --ignoreConfig example/zodv4/schemas.ts",
64+
"type-check:myzod": "tsc --strict --skipLibCheck --noEmit --ignoreConfig example/myzod/schemas.ts",
65+
"type-check:valibot": "tsc --strict --skipLibCheck --noEmit --ignoreConfig example/valibot/schemas.ts",
6666
"test": "vitest run",
6767
"build": "run-p build:*",
6868
"build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",

src/lazy.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { TypeNode } from 'graphql';
2+
import type { Visitor } from './visitor.js';
3+
4+
import { isEnumType, isScalarType } from 'graphql';
5+
import { isNamedType } from './graphql.js';
6+
7+
/**
8+
* Wraps a schema expression in a library-specific lazy reference when the type
9+
* is a complex (non-scalar, non-enum) named type — avoiding issues with
10+
* mutually-recursive input types.
11+
*
12+
* Each validation library has its own lazy syntax (z.lazy, v.lazy, etc.), so
13+
* callers supply the wrapper function.
14+
*
15+
* @param lazyWrapper - e.g. `(s) => \`z.lazy(() => ${s})\``
16+
*/
17+
export function buildMaybeLazy(
18+
visitor: Visitor,
19+
type: TypeNode,
20+
schema: string,
21+
lazyWrapper: (schema: string) => string,
22+
): string {
23+
if (!isNamedType(type))
24+
return schema;
25+
26+
const schemaType = visitor.getType(type.name.value);
27+
const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType);
28+
return isComplexType ? lazyWrapper(schema) : schema;
29+
}

src/myzod/index.ts

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import type { Visitor } from '../visitor.js';
1616
import { resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers';
1717
import { convertNameParts, DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
1818
import {
19-
isEnumType,
20-
isScalarType,
2119
Kind,
2220
} from 'graphql';
2321
import { buildApi, formatDirectiveConfig } from '../directive.js';
@@ -29,6 +27,8 @@ import {
2927
isNonNullType,
3028
ObjectTypeDefinitionBuilder,
3129
} from '../graphql.js';
30+
import { buildMaybeLazy } from '../lazy.js';
31+
import { buildScalarSchema } from '../scalar.js';
3232
import { BaseSchemaVisitor } from '../schema_visitor.js';
3333

3434
const anySchema = `definedNonNullAnySchema`;
@@ -365,33 +365,12 @@ function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visit
365365
}
366366

367367
function maybeLazy(visitor: Visitor, type: TypeNode, schema: string): string {
368-
if (!isNamedType(type)) {
369-
return schema;
370-
}
371-
372-
const schemaType = visitor.getType(type.name.value);
373-
const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType);
374-
return isComplexType ? `myzod.lazy(() => ${schema})` : schema;
368+
return buildMaybeLazy(visitor, type, schema, s => `myzod.lazy(() => ${s})`);
375369
}
376370

377371
function myzod4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string {
378-
if (config.scalarSchemas?.[scalarName])
379-
return config.scalarSchemas[scalarName];
380-
381-
const tsType = visitor.getScalarType(scalarName);
382-
switch (tsType) {
383-
case 'string':
384-
return `myzod.string()`;
385-
case 'number':
386-
return `myzod.number()`;
387-
case 'boolean':
388-
return `myzod.boolean()`;
389-
}
390-
391-
if (config.defaultScalarTypeSchema) {
392-
return config.defaultScalarTypeSchema;
393-
}
394-
395-
console.warn('unhandled name:', scalarName);
396-
return anySchema;
372+
return buildScalarSchema(config, visitor, scalarName, {
373+
typeMap: { string: 'myzod.string()', number: 'myzod.number()', boolean: 'myzod.boolean()' },
374+
fallback: anySchema,
375+
});
397376
}

src/scalar.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { ValidationSchemaPluginConfig } from './config.js';
2+
import type { Visitor } from './visitor.js';
3+
4+
/**
5+
* Builds a library-specific scalar schema expression.
6+
*
7+
* All five validation libraries follow the same pattern: check for a custom
8+
* `scalarSchemas` override, fall back to a built-in type map for the resolved
9+
* TypeScript type (string/number/boolean), then apply `defaultScalarTypeSchema`,
10+
* and finally warn and return the library fallback.
11+
*
12+
* The only per-library differences are the strings in `typeMap`, the `fallback`
13+
* value, and whether custom schemas need wrapping (yup appends `.defined()`).
14+
*/
15+
export function buildScalarSchema(
16+
config: ValidationSchemaPluginConfig,
17+
visitor: Visitor,
18+
scalarName: string,
19+
options: {
20+
typeMap: Record<'string' | 'number' | 'boolean', string>
21+
fallback: string
22+
wrapCustom?: (schema: string) => string
23+
},
24+
): string {
25+
if (config.scalarSchemas?.[scalarName]) {
26+
const custom = config.scalarSchemas[scalarName];
27+
return options.wrapCustom ? options.wrapCustom(custom) : custom;
28+
}
29+
30+
const tsType = visitor.getScalarType(scalarName);
31+
if (tsType != null && tsType in options.typeMap)
32+
return options.typeMap[tsType as keyof typeof options.typeMap];
33+
34+
if (config.defaultScalarTypeSchema)
35+
return config.defaultScalarTypeSchema;
36+
37+
console.warn('unhandled scalar name:', scalarName);
38+
return options.fallback;
39+
}

src/valibot/index.ts

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import type { ValidationSchemaPluginConfig } from '../config.js';
1414
import type { Visitor } from '../visitor.js';
1515
import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
1616

17-
import { isEnumType, isScalarType } from 'graphql';
1817
import { buildApiForValibot, formatDirectiveConfig } from '../directive.js';
1918
import {
2019
InterfaceTypeDefinitionBuilder,
@@ -23,6 +22,8 @@ import {
2322
isNonNullType,
2423
ObjectTypeDefinitionBuilder,
2524
} from '../graphql.js';
25+
import { buildMaybeLazy } from '../lazy.js';
26+
import { buildScalarSchema } from '../scalar.js';
2627
import { BaseSchemaVisitor } from '../schema_visitor.js';
2728

2829
export class ValibotSchemaVisitor extends BaseSchemaVisitor {
@@ -287,33 +288,12 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis
287288
}
288289

289290
function maybeLazy(visitor: Visitor, type: TypeNode, schema: string): string {
290-
if (!isNamedType(type)) {
291-
return schema;
292-
}
293-
294-
const schemaType = visitor.getType(type.name.value);
295-
const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType);
296-
return isComplexType ? `v.lazy(() => ${schema})` : schema;
291+
return buildMaybeLazy(visitor, type, schema, s => `v.lazy(() => ${s})`);
297292
}
298293

299294
function valibot4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string {
300-
if (config.scalarSchemas?.[scalarName])
301-
return config.scalarSchemas[scalarName];
302-
303-
const tsType = visitor.getScalarType(scalarName);
304-
switch (tsType) {
305-
case 'string':
306-
return `v.string()`;
307-
case 'number':
308-
return `v.number()`;
309-
case 'boolean':
310-
return `v.boolean()`;
311-
}
312-
313-
if (config.defaultScalarTypeSchema) {
314-
return config.defaultScalarTypeSchema;
315-
}
316-
317-
console.warn('unhandled scalar name:', scalarName);
318-
return 'v.any()';
295+
return buildScalarSchema(config, visitor, scalarName, {
296+
typeMap: { string: 'v.string()', number: 'v.number()', boolean: 'v.boolean()' },
297+
fallback: 'v.any()',
298+
});
319299
}

src/yup/index.ts

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import type { Visitor } from '../visitor.js';
1616
import { resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers';
1717
import { convertNameParts, DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
1818
import {
19-
isEnumType,
20-
isScalarType,
2119
Kind,
2220
} from 'graphql';
2321
import { buildApi, formatDirectiveConfig } from '../directive.js';
@@ -29,6 +27,8 @@ import {
2927
isNonNullType,
3028
ObjectTypeDefinitionBuilder,
3129
} from '../graphql.js';
30+
import { buildMaybeLazy } from '../lazy.js';
31+
import { buildScalarSchema } from '../scalar.js';
3232
import { BaseSchemaVisitor } from '../schema_visitor.js';
3333

3434
export class YupSchemaVisitor extends BaseSchemaVisitor {
@@ -391,33 +391,13 @@ function generateNameNodeYupSchema(config: ValidationSchemaPluginConfig, visitor
391391
}
392392

393393
function maybeLazy(visitor: Visitor, type: TypeNode, schema: string): string {
394-
if (!isNamedType(type)) {
395-
return schema;
396-
}
397-
398-
const schemaType = visitor.getType(type.name.value);
399-
const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType);
400-
return isComplexType ? `yup.lazy(() => ${schema})` : schema;
394+
return buildMaybeLazy(visitor, type, schema, s => `yup.lazy(() => ${s})`);
401395
}
402396

403397
function yup4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string {
404-
if (config.scalarSchemas?.[scalarName])
405-
return `${config.scalarSchemas[scalarName]}.defined()`;
406-
407-
const tsType = visitor.getScalarType(scalarName);
408-
switch (tsType) {
409-
case 'string':
410-
return `yup.string().defined()`;
411-
case 'number':
412-
return `yup.number().defined()`;
413-
case 'boolean':
414-
return `yup.boolean().defined()`;
415-
}
416-
417-
if (config.defaultScalarTypeSchema) {
418-
return config.defaultScalarTypeSchema
419-
}
420-
421-
console.warn('unhandled name:', scalarName);
422-
return `yup.mixed()`;
398+
return buildScalarSchema(config, visitor, scalarName, {
399+
typeMap: { string: 'yup.string().defined()', number: 'yup.number().defined()', boolean: 'yup.boolean().defined()' },
400+
fallback: 'yup.mixed()',
401+
wrapCustom: s => `${s}.defined()`,
402+
});
423403
}

0 commit comments

Comments
 (0)