From fa550786b00a66fb24e2f8ed53e92bc33d92a99e Mon Sep 17 00:00:00 2001 From: Jon Koops Date: Tue, 24 Mar 2026 12:51:20 +0100 Subject: [PATCH] fix: use schema-first generic pattern for Zod type compatibility Refactor `SignInResolverFactoryOptions` and `createSchemaFromZod` to use `TSchema extends ZodType` instead of `ZodSchema`, avoiding "excessively deep" TypeScript inference errors when multiple Zod copies are resolved in a project. Signed-off-by: Jon Koops --- .changeset/fix-zod-generic-auth-node.md | 26 ++++++++++++++++ .../fix-zod-generic-frontend-plugin-api.md | 5 ++++ .../src/schema/createSchemaFromZod.ts | 10 +++---- plugins/auth-node/report.api.md | 22 +++++--------- .../sign-in/createSignInResolverFactory.ts | 30 ++++++++----------- 5 files changed, 56 insertions(+), 37 deletions(-) create mode 100644 .changeset/fix-zod-generic-auth-node.md create mode 100644 .changeset/fix-zod-generic-frontend-plugin-api.md diff --git a/.changeset/fix-zod-generic-auth-node.md b/.changeset/fix-zod-generic-auth-node.md new file mode 100644 index 0000000000..d8ef7e4931 --- /dev/null +++ b/.changeset/fix-zod-generic-auth-node.md @@ -0,0 +1,26 @@ +--- +'@backstage/plugin-auth-node': minor +--- + +**BREAKING**: Refactored `SignInResolverFactoryOptions` to use a schema-first generic pattern, following Zod's [recommended approach](https://zod.dev/library-authors?id=how-to-accept-user-defined-schemas#how-to-accept-user-defined-schemas) for writing generic functions. The type parameters changed from `` to ``. + +This fixes "Type instantiation is excessively deep and possibly infinite" errors that occurred when the Zod version in a user's project did not align with the one in Backstage core. + +If you use `createSignInResolverFactory` without explicit type parameters (the typical usage), no changes are needed: + +```ts +// This usage is unchanged +createSignInResolverFactory({ + optionsSchema: z.object({ domain: z.string() }).optional(), + create(options = {}) { + /* ... */ + }, +}); +``` + +If you reference `SignInResolverFactoryOptions` with explicit type parameters, update as follows: + +```diff +- SignInResolverFactoryOptions ++ SignInResolverFactoryOptions +``` diff --git a/.changeset/fix-zod-generic-frontend-plugin-api.md b/.changeset/fix-zod-generic-frontend-plugin-api.md new file mode 100644 index 0000000000..8950ed70c9 --- /dev/null +++ b/.changeset/fix-zod-generic-frontend-plugin-api.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Refactored the internal `createSchemaFromZod` helper to use a schema-first generic pattern, replacing the `ZodSchema` constraint with `TSchema extends ZodType`. This avoids "excessively deep" type inference errors when multiple Zod copies are resolved. diff --git a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts index 6dbac61c62..6fc7e3fd02 100644 --- a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts +++ b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts @@ -15,16 +15,16 @@ */ import { JsonObject } from '@backstage/types'; -import { z, type ZodSchema, type ZodTypeDef } from 'zod/v3'; +import { z, type ZodIssue, type ZodType } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { PortableSchema } from './types'; /** * @internal */ -export function createSchemaFromZod( - schemaCreator: (zImpl: typeof z) => ZodSchema, -): PortableSchema { +export function createSchemaFromZod( + schemaCreator: (zImpl: typeof z) => TSchema, +): PortableSchema, z.input> { const schema = schemaCreator(z); return { // TODO: Types allow z.array etc here but it will break stuff @@ -41,7 +41,7 @@ export function createSchemaFromZod( }; } -function formatIssue(issue: z.ZodIssue): string { +function formatIssue(issue: ZodIssue): string { if (issue.code === 'invalid_union') { return formatIssue(issue.unionErrors[0].issues[0]); } diff --git a/plugins/auth-node/report.api.md b/plugins/auth-node/report.api.md index 23565164a4..b360c2ae6a 100644 --- a/plugins/auth-node/report.api.md +++ b/plugins/auth-node/report.api.md @@ -16,8 +16,8 @@ import { Profile } from 'passport'; import { Request as Request_2 } from 'express'; import { Response as Response_2 } from 'express'; import { Strategy } from 'passport'; -import type { ZodSchema } from 'zod/v3'; -import type { ZodTypeDef } from 'zod/v3'; +import type { z } from 'zod/v3'; +import type { ZodType } from 'zod/v3'; // @public (undocumented) export interface AuthOwnershipResolutionExtensionPoint { @@ -227,15 +227,10 @@ export function createProxyAuthRouteHandlers( // @public (undocumented) export function createSignInResolverFactory< TAuthResult, - TOptionsOutput, - TOptionsInput, + TSchema extends ZodType = ZodType, >( - options: SignInResolverFactoryOptions< - TAuthResult, - TOptionsOutput, - TOptionsInput - >, -): SignInResolverFactory; + options: SignInResolverFactoryOptions, +): SignInResolverFactory>; // @public (undocumented) export function decodeOAuthState(encodedState: string): OAuthState; @@ -676,13 +671,12 @@ export interface SignInResolverFactory { // @public (undocumented) export interface SignInResolverFactoryOptions< TAuthResult, - TOptionsOutput, - TOptionsInput, + TSchema extends ZodType = ZodType, > { // (undocumented) - create(options: TOptionsOutput): SignInResolver; + create(options: z.output): SignInResolver; // (undocumented) - optionsSchema?: ZodSchema; + optionsSchema?: TSchema; } // @public diff --git a/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts index a74a1d4bfd..3f1dc1eee9 100644 --- a/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts +++ b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ZodSchema, ZodTypeDef } from 'zod/v3'; +import type { z, ZodType } from 'zod/v3'; import { SignInResolver } from '../types'; import zodToJsonSchema from 'zod-to-json-schema'; import { JsonObject } from '@backstage/types'; @@ -34,38 +34,32 @@ export interface SignInResolverFactory { /** @public */ export interface SignInResolverFactoryOptions< TAuthResult, - TOptionsOutput, - TOptionsInput, + TSchema extends ZodType = ZodType, > { - optionsSchema?: ZodSchema; - create(options: TOptionsOutput): SignInResolver; + optionsSchema?: TSchema; + create(options: z.output): SignInResolver; } /** @public */ export function createSignInResolverFactory< TAuthResult, - TOptionsOutput, - TOptionsInput, + TSchema extends ZodType = ZodType, >( - options: SignInResolverFactoryOptions< - TAuthResult, - TOptionsOutput, - TOptionsInput - >, -): SignInResolverFactory { + options: SignInResolverFactoryOptions, +): SignInResolverFactory> { const { optionsSchema } = options; if (!optionsSchema) { - return (resolverOptions?: TOptionsInput) => { + return (resolverOptions?: z.input) => { if (resolverOptions) { throw new InputError('sign-in resolver does not accept options'); } - return options.create(undefined as TOptionsOutput); + return options.create(undefined); }; } const factory = ( - ...[resolverOptions]: undefined extends TOptionsInput - ? [options?: TOptionsInput] - : [options: TOptionsInput] + ...[resolverOptions]: undefined extends z.input + ? [options?: z.input] + : [options: z.input] ) => { let parsedOptions; try {