From 02fced51db0e4f3f8f88da0fe9eb02dbea18d1f6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 7 Sep 2025 13:41:16 +0200 Subject: [PATCH] frontend-app-api: rework app error types Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/report.api.md | 102 +++++++++++-- .../src/tree/instantiateAppNodeTree.test.ts | 2 +- .../src/tree/instantiateAppNodeTree.ts | 4 +- .../src/wiring/createErrorCollector.ts | 140 ++++++++++-------- packages/frontend-app-api/src/wiring/index.ts | 2 +- 5 files changed, 174 insertions(+), 76 deletions(-) diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index 1806f47b02..fca32ed38c 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -5,7 +5,6 @@ ```ts import { ApiHolder } from '@backstage/core-plugin-api'; import { AppNode } from '@backstage/frontend-plugin-api'; -import { AppNodeSpec } from '@backstage/frontend-plugin-api'; import { AppTree } from '@backstage/frontend-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { ExtensionFactoryMiddleware } from '@backstage/frontend-plugin-api'; @@ -18,16 +17,97 @@ import { RouteRef } from '@backstage/frontend-plugin-api'; import { SubRouteRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) -export type AppError = { - code: string; - message: string; - context?: { - node?: AppNode; - spec?: AppNodeSpec; - plugin?: FrontendPlugin; - extensionId?: string; - inputName?: string; - dataRefId?: string; +export type AppError = + keyof AppErrorTypes extends infer ICode extends keyof AppErrorTypes + ? ICode extends any + ? { + code: ICode; + message: string; + context: AppErrorTypes[ICode]['context']; + } + : never + : never; + +// @public (undocumented) +export type AppErrorTypes = { + EXTENSION_IGNORED: { + context: { + plugin: FrontendPlugin; + extensionId: string; + }; + }; + INVALID_EXTENSION_CONFIG_KEY: { + context: { + extensionId: string; + }; + }; + EXTENSION_INPUT_REDIRECT_CONFLICT: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_INPUT_DATA_IGNORED: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_INPUT_DATA_MISSING: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_ATTACHMENT_CONFLICT: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_ATTACHMENT_MISSING: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_CONFIGURATION_INVALID: { + context: { + node: AppNode; + }; + }; + EXTENSION_INVALID: { + context: { + node: AppNode; + }; + }; + EXTENSION_OUTPUT_CONFLICT: { + context: { + node: AppNode; + dataRefId: string; + }; + }; + EXTENSION_OUTPUT_MISSING: { + context: { + node: AppNode; + dataRefId: string; + }; + }; + EXTENSION_OUTPUT_IGNORED: { + context: { + node: AppNode; + dataRefId: string; + }; + }; + EXTENSION_FACTORY_ERROR: { + context: { + node: AppNode; + }; + }; + API_EXTENSION_INVALID: { + context: { + node: AppNode; + }; }; }; diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 8f892afd56..13ec6e8a59 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -1098,7 +1098,7 @@ describe('instantiateAppNodeTree', () => { }); const errors = collector.collectErrors(); for (const error of errors ?? []) { - expect(error.context?.node).toBe(node); + expect('node' in error.context && error.context.node).toBe(node); } return errors; } diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index f8e18cdcbd..39fc88c19a 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -95,7 +95,7 @@ function resolveInputDataContainer( extensionData: Array, attachment: AppNode, inputName: string, - collector: ErrorCollector<'node' | 'inputName'>, + collector: ErrorCollector<{ node: AppNode; inputName: string }>, ): { node: AppNode } & ExtensionDataContainer { const dataMap = new Map(); @@ -250,7 +250,7 @@ function resolveV2Inputs( >; }, attachments: ReadonlyMap, - parentCollector: ErrorCollector<'node'>, + parentCollector: ErrorCollector<{ node: AppNode }>, ): ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput< ExtensionDataRef, diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts index fd028f1f18..633febd945 100644 --- a/packages/frontend-app-api/src/wiring/createErrorCollector.ts +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -15,63 +15,81 @@ */ import { AppNode, FrontendPlugin } from '@backstage/frontend-plugin-api'; -import { Expand } from '@backstage/types'; -type AppErrorTypes = { +/** + * @public + */ +export type AppErrorTypes = { // resolveAppNodeSpecs - EXTENSION_IGNORED: 'plugin' | 'extensionId'; - INVALID_EXTENSION_CONFIG_KEY: 'extensionId'; - // resolveAppTree - EXTENSION_INPUT_REDIRECT_CONFLICT: 'node' | 'inputName'; - // instantiateAppNodeTree - EXTENSION_INPUT_DATA_IGNORED: 'node' | 'inputName'; - EXTENSION_INPUT_DATA_MISSING: 'node' | 'inputName'; - EXTENSION_ATTACHMENT_CONFLICT: 'node' | 'inputName'; - EXTENSION_ATTACHMENT_MISSING: 'node' | 'inputName'; - EXTENSION_CONFIGURATION_INVALID: 'node'; - EXTENSION_INVALID: 'node'; - EXTENSION_OUTPUT_CONFLICT: 'node' | 'dataRefId'; - EXTENSION_OUTPUT_MISSING: 'node' | 'dataRefId'; - EXTENSION_OUTPUT_IGNORED: 'node' | 'dataRefId'; - EXTENSION_FACTORY_ERROR: 'node'; - // createSpecializedApp - API_EXTENSION_INVALID: 'node'; -}; - -type AppErrorContext = { - node?: AppNode; - plugin?: FrontendPlugin; - extensionId?: string; - dataRefId?: string; - inputName?: string; -}; - -/** @public */ -export type AppError = - { - code: TCode; - message: string; - context: Expand< - AppErrorContext & - Pick< - Required, - keyof AppErrorTypes extends TCode ? never : AppErrorTypes[TCode] - > - >; + EXTENSION_IGNORED: { + context: { plugin: FrontendPlugin; extensionId: string }; }; + INVALID_EXTENSION_CONFIG_KEY: { + context: { extensionId: string }; + }; + // resolveAppTree + EXTENSION_INPUT_REDIRECT_CONFLICT: { + context: { node: AppNode; inputName: string }; + }; + // instantiateAppNodeTree + EXTENSION_INPUT_DATA_IGNORED: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_INPUT_DATA_MISSING: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_ATTACHMENT_CONFLICT: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_ATTACHMENT_MISSING: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_CONFIGURATION_INVALID: { + context: { node: AppNode }; + }; + EXTENSION_INVALID: { + context: { node: AppNode }; + }; + EXTENSION_OUTPUT_CONFLICT: { + context: { node: AppNode; dataRefId: string }; + }; + EXTENSION_OUTPUT_MISSING: { + context: { node: AppNode; dataRefId: string }; + }; + EXTENSION_OUTPUT_IGNORED: { + context: { node: AppNode; dataRefId: string }; + }; + EXTENSION_FACTORY_ERROR: { + context: { node: AppNode }; + }; + // createSpecializedApp + API_EXTENSION_INVALID: { + context: { node: AppNode }; + }; +}; + +/** + * @public + */ +export type AppError = + keyof AppErrorTypes extends infer ICode extends keyof AppErrorTypes + ? ICode extends any + ? { + code: ICode; + message: string; + context: AppErrorTypes[ICode]['context']; + } + : never + : never; /** @internal */ -export interface ErrorCollector< - TContext extends keyof AppErrorContext = never, -> { - // Type-only: here to make sure that all required keys are present - $$contextKeys: { [K in TContext]: K }; +export interface ErrorCollector { report( - report: Exclude< - AppErrorTypes[TCode], - TContext - > extends infer IContext extends keyof AppErrorContext - ? [IContext] extends [never] + report: Omit< + AppErrorTypes[TCode]['context'], + keyof TContext + > extends infer IContext extends {} + ? [{}] extends [IContext] ? { code: TCode; message: string; @@ -79,28 +97,28 @@ export interface ErrorCollector< : { code: TCode; message: string; - context: Pick, IContext>; + context: IContext; } : never, ): void; - child( - context?: TAdditionalContext & AppErrorContext, - ): ErrorCollector< - (TContext | keyof TAdditionalContext) & keyof AppErrorContext - >; + child( + context: TAdditionalContext, + ): ErrorCollector; collectErrors(): AppError[] | undefined; } /** @internal */ export function createErrorCollector( - context?: AppError['context'], + context?: Partial, ): ErrorCollector { const errors: AppError[] = []; const children: ErrorCollector[] = []; return { - $$contextKeys: null as any, - report(report) { - errors.push({ ...report, context: { ...context, ...report.context } }); + report(report: { code: string; message: string; context?: {} }) { + errors.push({ + ...report, + context: { ...context, ...report.context }, + } as AppError); }, collectErrors() { const allErrors = [ diff --git a/packages/frontend-app-api/src/wiring/index.ts b/packages/frontend-app-api/src/wiring/index.ts index ce7574852a..1e18859ddd 100644 --- a/packages/frontend-app-api/src/wiring/index.ts +++ b/packages/frontend-app-api/src/wiring/index.ts @@ -19,4 +19,4 @@ export { type CreateSpecializedAppOptions, } from './createSpecializedApp'; export { type FrontendPluginInfoResolver } from './createPluginInfoAttacher'; -export { type AppError } from './createErrorCollector'; +export { type AppError, type AppErrorTypes } from './createErrorCollector';