From f217918b14d981b95d69131bddb743bf19cbd123 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 19 Nov 2024 12:01:44 +0100 Subject: [PATCH] chore: refactor to use opaque type helper instead Signed-off-by: blam --- .../src/components/scaffolder/decorators.ts | 2 +- packages/scaffolder-internal/package.json | 1 + .../src/wiring/InternalFormDecorator.ts | 42 ++++++++++++ .../scaffolder-internal/src/wiring/index.ts | 1 + .../src/next/components/Workflow/Workflow.tsx | 4 +- .../createScaffolderFormDecorator.test.ts | 4 +- .../createScaffolderFormDecorator.ts | 65 ++++++++++--------- .../src/alpha/api/FormDecoratorsApi.ts | 8 +-- plugins/scaffolder/src/alpha/api/types.ts | 4 +- .../alpha/hooks/useFormDecorators.test.tsx | 4 +- .../src/alpha/hooks/useFormDecorators.ts | 36 +++++----- yarn.lock | 1 + 12 files changed, 110 insertions(+), 62 deletions(-) create mode 100644 packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts diff --git a/packages/app/src/components/scaffolder/decorators.ts b/packages/app/src/components/scaffolder/decorators.ts index 6cce3a3fe0..7516a861b2 100644 --- a/packages/app/src/components/scaffolder/decorators.ts +++ b/packages/app/src/components/scaffolder/decorators.ts @@ -26,7 +26,7 @@ export const mockDecorator = createScaffolderFormDecorator({ deps: { githubApi: githubAuthApiRef, }, - fn: async ({ setSecrets }, { githubApi }) => { + decorator: async ({ setSecrets }, { githubApi }) => { const token = await githubApi.getAccessToken(); setSecrets(state => ({ ...state, GITHUB_TOKEN: token })); }, diff --git a/packages/scaffolder-internal/package.json b/packages/scaffolder-internal/package.json index d07614f127..9350fa7ac1 100644 --- a/packages/scaffolder-internal/package.json +++ b/packages/scaffolder-internal/package.json @@ -23,6 +23,7 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-scaffolder-react": "workspace:^", "zod": "^3.22.4" }, diff --git a/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts b/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts new file mode 100644 index 0000000000..7942544acb --- /dev/null +++ b/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { OpaqueType } from '@internal/opaque'; +import { z } from 'zod'; + +import { + ScaffolderFormDecorator, + ScaffolderFormDecoratorContext, +} from '@backstage/plugin-scaffolder-react/alpha'; +import { AnyApiRef } from '@backstage/frontend-plugin-api'; + +/** @alpha */ +export const OpaqueFormDecorator = OpaqueType.create<{ + public: ScaffolderFormDecorator; + versions: { + readonly version: 'v1'; + readonly id: string; + readonly schema?: { + input?: { + [key in string]: (zImpl: typeof z) => z.ZodType; + }; + }; + readonly deps?: { [key in string]: AnyApiRef }; + readonly decorator: ( + ctx: ScaffolderFormDecoratorContext, + deps: { [depName in string]: AnyApiRef['T'] }, + ) => Promise; + }; +}>({ type: '@backstage/scaffolder/FormDecorator', versions: ['v1'] }); diff --git a/packages/scaffolder-internal/src/wiring/index.ts b/packages/scaffolder-internal/src/wiring/index.ts index c85755bccf..37c8bd09cb 100644 --- a/packages/scaffolder-internal/src/wiring/index.ts +++ b/packages/scaffolder-internal/src/wiring/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { OpaqueFormField, type FormField } from './InternalFormField'; +export { OpaqueFormDecorator } from './InternalFormDecorator'; diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx index 91bc98dad3..f984f5ff7a 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx @@ -106,7 +106,7 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => { return; } - await formDecorator.fn({ + await formDecorator.decorator({ setSecrets, setFormState: ( handler: ( @@ -116,7 +116,7 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => { formState = { ...handler(formState) }; }, formState, - input: decorator.input, + input: decorator.input ?? {}, }); }), ); diff --git a/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.test.ts b/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.test.ts index cef533d283..e8157c9d9e 100644 --- a/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.test.ts +++ b/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.test.ts @@ -20,7 +20,7 @@ describe('createScaffolderFormDecorator', () => { const decorator = createScaffolderFormDecorator({ id: 'test', deps: {}, - fn: async () => {}, + decorator: async () => {}, }); expect(decorator).toMatchInlineSnapshot(` @@ -43,7 +43,7 @@ describe('createScaffolderFormDecorator', () => { age: z => z.number(), }, }, - fn: async ctx => { + decorator: async ctx => { const name: string = ctx.input.name; // @ts-expect-error diff --git a/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts b/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts index 2c0a43b24d..0064ae0397 100644 --- a/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts +++ b/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts @@ -14,11 +14,14 @@ * limitations under the License. */ import { AnyApiRef } from '@backstage/core-plugin-api'; -import { JsonValue } from '@backstage/types'; +import { JsonObject, JsonValue } from '@backstage/types'; +import { OpaqueFormDecorator } from '@internal/scaffolder'; import { z } from 'zod'; /** @alpha */ -export type ScaffolderFormDecoratorContext = { +export type ScaffolderFormDecoratorContext< + TInput extends JsonObject = JsonObject, +> = { input: TInput; formState: Record; @@ -31,53 +34,51 @@ export type ScaffolderFormDecoratorContext = { }; /** @alpha */ -export type ScaffolderFormDecorator< - TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, - TDeps extends { [key in string]: AnyApiRef }, -> = { - version: 'v1'; - id: string; - schema?: { - input?: TInputSchema; - }; - deps?: TDeps; - fn: ( - ctx: ScaffolderFormDecoratorContext<{ - [key in keyof TInputSchema]: z.infer>; - }>, - deps: TDeps extends { [key in string]: AnyApiRef } - ? { [key in keyof TDeps]: TDeps[key]['T'] } - : never, - ) => Promise; +export type ScaffolderFormDecorator = { + readonly $$type: '@backstage/scaffolder/FormDecorator'; + readonly id: string; + readonly TInput: TInput; }; -/** @alpha */ -export type AnyScaffolderFormDecorator = ScaffolderFormDecorator; /** * Method for creating decorators which can be used to collect * secrets from the user before submitting to the backend. * @alpha */ export function createScaffolderFormDecorator< - TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, - TDeps extends { [key in string]: AnyApiRef }, + TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } = { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + TDeps extends { [key in string]: AnyApiRef } = { [key in string]: AnyApiRef }, + TInput extends JsonObject = { + [key in keyof TInputSchema]: z.infer>; + }, >(options: { id: string; schema?: { input?: TInputSchema; }; deps?: TDeps; - fn: ( - ctx: ScaffolderFormDecoratorContext<{ - [key in keyof TInputSchema]: z.infer>; - }>, + decorator: ( + ctx: ScaffolderFormDecoratorContext, deps: TDeps extends { [key in string]: AnyApiRef } ? { [key in keyof TDeps]: TDeps[key]['T'] } : never, ) => Promise; -}): ScaffolderFormDecorator { - return { +}): ScaffolderFormDecorator { + return OpaqueFormDecorator.createInstance('v1', { ...options, - version: 'v1', - }; + TInput: null as unknown as TInput, + } as { + id: string; + schema?: { + input?: TInputSchema; + }; + TInput: TInput; + deps?: TDeps; + decorator: ( + ctx: ScaffolderFormDecoratorContext, + deps: { [key in string]: AnyApiRef['T'] }, + ) => Promise; + }); } diff --git a/plugins/scaffolder/src/alpha/api/FormDecoratorsApi.ts b/plugins/scaffolder/src/alpha/api/FormDecoratorsApi.ts index 79ec618ce2..13c4b6d1a2 100644 --- a/plugins/scaffolder/src/alpha/api/FormDecoratorsApi.ts +++ b/plugins/scaffolder/src/alpha/api/FormDecoratorsApi.ts @@ -15,7 +15,7 @@ */ import { ScaffolderFormDecoratorsApi } from './types'; -import { AnyScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha'; +import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha'; /** @alpha */ export class DefaultScaffolderFormDecoratorsApi @@ -23,17 +23,17 @@ export class DefaultScaffolderFormDecoratorsApi { private constructor( private readonly options: { - decorators: Array; + decorators: Array; }, ) {} - static create(options?: { decorators: AnyScaffolderFormDecorator[] }) { + static create(options?: { decorators: ScaffolderFormDecorator[] }) { return new DefaultScaffolderFormDecoratorsApi( options ?? { decorators: [] }, ); } - async getFormDecorators(): Promise { + async getFormDecorators(): Promise { return this.options.decorators; } } diff --git a/plugins/scaffolder/src/alpha/api/types.ts b/plugins/scaffolder/src/alpha/api/types.ts index d52d102d6d..28ce8dab3d 100644 --- a/plugins/scaffolder/src/alpha/api/types.ts +++ b/plugins/scaffolder/src/alpha/api/types.ts @@ -16,7 +16,7 @@ import { FormFieldExtensionData, - AnyScaffolderFormDecorator, + ScaffolderFormDecorator, } from '@backstage/plugin-scaffolder-react/alpha'; export interface ScaffolderFormFieldsApi { @@ -24,5 +24,5 @@ export interface ScaffolderFormFieldsApi { } export interface ScaffolderFormDecoratorsApi { - getFormDecorators(): Promise; + getFormDecorators(): Promise; } diff --git a/plugins/scaffolder/src/alpha/hooks/useFormDecorators.test.tsx b/plugins/scaffolder/src/alpha/hooks/useFormDecorators.test.tsx index 091e2fbc42..66e2389a45 100644 --- a/plugins/scaffolder/src/alpha/hooks/useFormDecorators.test.tsx +++ b/plugins/scaffolder/src/alpha/hooks/useFormDecorators.test.tsx @@ -38,7 +38,7 @@ describe('useFormDecorators', () => { test: z => z.string(), }, }, - async fn({ input: { test } }, { mockApiRef: mock }) { + async decorator({ input: { test } }, { mockApiRef: mock }) { mock.test(test); }, }); @@ -70,7 +70,7 @@ describe('useFormDecorators', () => { const testDecorator = result.get('test')!; expect(testDecorator).toBeDefined(); - await testDecorator.fn({ + await testDecorator.decorator({ formState: {}, setFormState: () => {}, setSecrets: () => {}, diff --git a/plugins/scaffolder/src/alpha/hooks/useFormDecorators.ts b/plugins/scaffolder/src/alpha/hooks/useFormDecorators.ts index 50fc6bfde0..c0dbe4b354 100644 --- a/plugins/scaffolder/src/alpha/hooks/useFormDecorators.ts +++ b/plugins/scaffolder/src/alpha/hooks/useFormDecorators.ts @@ -13,19 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AnyApiRef, useApi, useApiHolder } from '@backstage/core-plugin-api'; +import { errorApiRef, useApi, useApiHolder } from '@backstage/core-plugin-api'; import { formDecoratorsApiRef } from '../api/ref'; import useAsync from 'react-use/esm/useAsync'; import { useMemo } from 'react'; import { ScaffolderFormDecoratorContext } from '@backstage/plugin-scaffolder-react/alpha'; +import { OpaqueFormDecorator } from '@internal/scaffolder'; /** @internal */ type BoundFieldDecorator = { - fn: (ctx: ScaffolderFormDecoratorContext) => Promise; + decorator: (ctx: ScaffolderFormDecoratorContext) => Promise; }; export const useFormDecorators = () => { const formDecoratorsApi = useApi(formDecoratorsApiRef); + const errorApi = useApi(errorApiRef); const { value: decorators } = useAsync( () => formDecoratorsApi.getFormDecorators(), [], @@ -37,27 +39,27 @@ export const useFormDecorators = () => { for (const decorator of decorators ?? []) { try { - const resolvedDeps = Object.entries(decorator.deps ?? {}).map( - ([key, value]) => { - const api = apiHolder.get(value); - if (!api) { - throw new Error( - `Failed to resolve apiRef ${value.id} for form decorator ${decorator.id} it will be disabled`, - ); - } - return [key, api]; - }, - ); + const { decorator: decoratorFn, deps } = + OpaqueFormDecorator.toInternal(decorator); + + const resolvedDeps = Object.entries(deps ?? {}).map(([key, value]) => { + const api = apiHolder.get(value); + if (!api) { + throw new Error( + `Failed to resolve apiRef ${value.id} for form decorator ${decorator.id} it will be disabled`, + ); + } + return [key, api]; + }); decoratorsMap.set(decorator.id, { - fn: ctx => decorator.fn(ctx, Object.fromEntries(resolvedDeps)), + decorator: ctx => decoratorFn(ctx, Object.fromEntries(resolvedDeps)), }); } catch (ex) { - // eslint-disable-next-line no-console - console.error(ex); + errorApi.post(ex); return undefined; } } return decoratorsMap; - }, [apiHolder, decorators]); + }, [apiHolder, decorators, errorApi]); }; diff --git a/yarn.lock b/yarn.lock index 4ed6c78663..d0f61dedd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10339,6 +10339,7 @@ __metadata: resolution: "@internal/scaffolder@workspace:packages/scaffolder-internal" dependencies: "@backstage/cli": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-scaffolder-react": "workspace:^" zod: ^3.22.4 languageName: unknown