diff --git a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx
index 7d06d55a92..b2ab5d5aef 100644
--- a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx
+++ b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx
@@ -18,9 +18,9 @@ import { createComponentRef } from '@backstage/frontend-plugin-api';
import { DefaultComponentsApi } from './DefaultComponentsApi';
import { render, screen } from '@testing-library/react';
-const testRefA = createComponentRef({ id: 'test.a' });
-const testRefB1 = createComponentRef({ id: 'test.b' });
-const testRefB2 = createComponentRef({ id: 'test.b' });
+const testRefA = createComponentRef({ id: 'test.a', mode: 'sync' });
+const testRefB1 = createComponentRef({ id: 'test.b', mode: 'sync' });
+const testRefB2 = createComponentRef({ id: 'test.b', mode: 'sync' });
describe('DefaultComponentsApi', () => {
it('should provide components', () => {
diff --git a/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.test.tsx
new file mode 100644
index 0000000000..b0095e4350
--- /dev/null
+++ b/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.test.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 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 { createComponentRef } from '../components';
+import { ComponentImplementationBlueprint } from './ComponentImplementationBlueprint';
+
+describe('ComponentImplementationBlueprint', () => {
+ it('should allow defining a component override for sync component ref', () => {
+ const componentRef = createComponentRef({
+ id: 'test.component',
+ mode: 'sync',
+ defaultComponent: (props: { hello: string }) =>
{props.hello}
,
+ });
+
+ const extension = ComponentImplementationBlueprint.make({
+ params: define =>
+ define({
+ ref: componentRef,
+ component: props => {
+ // @ts-expect-error
+ const t: number = props.hello;
+
+ return Override {props.hello}
;
+ },
+ }),
+ });
+
+ expect(extension).toBeDefined();
+ });
+});
diff --git a/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.ts
new file mode 100644
index 0000000000..d903d63f5e
--- /dev/null
+++ b/packages/frontend-plugin-api/src/blueprints/ComponentImplementationBlueprint.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2025 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 { ComponentRef } from '../components';
+import {
+ createExtensionBlueprint,
+ createExtensionBlueprintParams,
+ createExtensionDataRef,
+} from '../wiring';
+
+export const componentDataRef = createExtensionDataRef<{
+ ref: ComponentRef;
+ component:
+ | ((props: {}) => JSX.Element | null)
+ | (() => Promise<(props: {}) => JSX.Element | null>);
+ type: 'sync' | 'async';
+}>().with({ id: 'core.component.component' });
+
+export const ComponentImplementationBlueprint = createExtensionBlueprint({
+ kind: 'component',
+ attachTo: { id: 'api:app/components', input: 'components' },
+ output: [componentDataRef],
+ dataRefs: {
+ component: componentDataRef,
+ },
+ defineParams[>(params: {
+ ref: Ref;
+ component: Ref extends ComponentRef<
+ infer IInnerComponentProps,
+ any,
+ infer IMode
+ >
+ ? IMode extends 'sync'
+ ? (props: IInnerComponentProps) => JSX.Element
+ : IMode extends 'async'
+ ? () => Promise<(props: IInnerComponentProps) => JSX.Element>
+ : never
+ : never;
+ }) {
+ return createExtensionBlueprintParams(params);
+ },
+ *factory(params) {
+ yield componentDataRef({
+ ref: params.ref,
+ component: params.component,
+ type: params.ref.mode,
+ });
+ },
+});
diff --git a/packages/frontend-plugin-api/src/components/createComponentRef.tsx b/packages/frontend-plugin-api/src/components/createComponentRef.tsx
index ebf707fcca..f0a1e36371 100644
--- a/packages/frontend-plugin-api/src/components/createComponentRef.tsx
+++ b/packages/frontend-plugin-api/src/components/createComponentRef.tsx
@@ -20,27 +20,27 @@ import { OpaqueComponentRef } from '@internal/frontend';
export type ComponentRef<
TInnerComponentProps extends {} = {},
TExternalComponentProps extends {} = TInnerComponentProps,
+ TMode extends 'sync' | 'async' = 'sync' | 'async',
> = {
id: string;
TProps: TInnerComponentProps;
TExternalProps: TExternalComponentProps;
+ TMode: TMode;
$$type: '@backstage/ComponentRef';
};
-export interface ComponentRefOptions<
+export type ComponentRefOptions<
TInnerComponentProps extends {},
- TExternalComponentProps extends {},
- TMode extends 'sync' | 'async',
-> {
+ TExternalComponentProps extends {} = TInnerComponentProps,
+> = {
id: string;
- mode: TMode;
- defaultComponent?: TMode extends 'async'
- ? () => Promise<(props: TInnerComponentProps) => JSX.Element>
+ componentAsync: TMode extends 'async'
+ ? () => Promise<(props: TInnerComponentProps) => JSX.Element | null>
: TMode extends 'sync'
- ? (props: TInnerComponentProps) => JSX.Element
+ ? (props: TInnerComponentProps) => JSX.Element | null
: never;
transformProps?: (props: TExternalComponentProps) => TInnerComponentProps;
-}
+};
/**
* Creates a new component ref that is synchronous.
@@ -55,7 +55,7 @@ export function createComponentRef<
TExternalComponentProps,
'sync'
>,
-): ComponentRef;
+): ComponentRef;
/**
* Creates a new component ref that is asynchronous.
@@ -70,22 +70,24 @@ export function createComponentRef<
TExternalComponentProps,
'async'
>,
-): ComponentRef;
+): ComponentRef;
export function createComponentRef<
TInnerComponentProps extends {},
- TExternalComponentProps extends {},
+ TExternalComponentProps extends {} = TInnerComponentProps,
+ TMode extends 'sync' | 'async' = 'sync' | 'async',
>(
options: ComponentRefOptions<
TInnerComponentProps,
TExternalComponentProps,
- 'async' | 'sync'
+ TMode
>,
): ComponentRef {
return OpaqueComponentRef.createInstance('v1', {
id: options.id,
TProps: null as unknown as TInnerComponentProps,
TExternalProps: null as unknown as TExternalComponentProps,
+ TMode: null as unknown as TMode,
toString() {
return `ComponentRef{id=${options.id}}`;
},
diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts
index f94551755f..fd9fb3b9f5 100644
--- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts
+++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts
@@ -214,7 +214,6 @@ type AnyParamsInput =
* @public
*/
export interface ExtensionBlueprint<
- // TParamsMapper extends (params: any) => object,
T extends ExtensionBlueprintParameters = ExtensionBlueprintParameters,
> {
dataRefs: T['dataRefs'];
]