diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx
new file mode 100644
index 0000000000..d841ea2c62
--- /dev/null
+++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.test.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 React from 'react';
+import { coreExtensionData } from './coreExtensionData';
+import { createExtensionKind } from './createExtensionKind';
+
+describe('createExtensionKind', () => {
+ it('should allow creation of extension kinds', () => {
+ const TestExtension = createExtensionKind({
+ kind: 'test-extension',
+ attachTo: { id: 'test', input: 'default' },
+ output: {
+ element: coreExtensionData.reactElement,
+ },
+ factory(_, props: { text: string }) {
+ return {
+ element:
{props.text}
,
+ };
+ },
+ });
+
+ const extension = TestExtension.new({
+ name: 'my-extension',
+ props: {
+ text: 'Hello, world!',
+ },
+ });
+
+ expect(extension).toBeDefined();
+ });
+});
diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts
new file mode 100644
index 0000000000..31a8e5da64
--- /dev/null
+++ b/packages/frontend-plugin-api/src/wiring/createExtensionKind.ts
@@ -0,0 +1,164 @@
+import { AppNode } from '../apis';
+import { PortableSchema } from '../schema';
+import { Expand } from '../types';
+import {
+ AnyExtensionDataMap,
+ AnyExtensionInputMap,
+ ExtensionDataValues,
+ ResolvedExtensionInputs,
+ createExtension,
+} from './createExtension';
+
+/*
+ * 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.
+ */
+interface ExtensionKindOptions<
+ TProps,
+ TInputs extends AnyExtensionInputMap,
+ TOutput extends AnyExtensionDataMap,
+ TConfig,
+> {
+ kind: string;
+ namespace?: string;
+ name?: string;
+ attachTo: { id: string; input: string };
+ disabled?: boolean;
+ inputs?: TInputs;
+ output: TOutput;
+ configSchema?: PortableSchema;
+ factory(
+ options: {
+ node: AppNode;
+ config: TConfig;
+ inputs: Expand>;
+ },
+ props: TProps,
+ ): Expand>;
+}
+
+class ExtensionKind<
+ TProps,
+ TInputs extends AnyExtensionInputMap,
+ TOutput extends AnyExtensionDataMap,
+ TConfig,
+> {
+ static create<
+ TProps,
+ TInputs extends AnyExtensionInputMap,
+ TOutput extends AnyExtensionDataMap,
+ TConfig,
+ >(
+ options: ExtensionKindOptions,
+ ): ExtensionKind {
+ return new ExtensionKind(options);
+ }
+
+ private constructor(
+ private readonly options: ExtensionKindOptions<
+ TProps,
+ TInputs,
+ TOutput,
+ TConfig
+ >,
+ ) {}
+
+ public new(options: {
+ namespace?: string;
+ name?: string;
+ attachTo?: { id: string; input: string };
+ disabled?: boolean;
+ inputs?: TInputs;
+ output?: TOutput;
+ configSchema?: PortableSchema;
+ props: TProps;
+ factory?(
+ options: {
+ node: AppNode;
+ config: TConfig;
+ inputs: Expand>;
+ orignalFactory(
+ options?: {
+ node?: AppNode;
+ config?: TConfig;
+ inputs?: Expand>;
+ },
+ props?: TProps,
+ ): Expand>;
+ },
+ props: TProps,
+ ): Expand>;
+ }) {
+ return createExtension({
+ kind: this.options.kind,
+ namespace: options.namespace ?? this.options.namespace,
+ name: options.name ?? this.options.name,
+ attachTo: options.attachTo ?? this.options.attachTo,
+ disabled: options.disabled ?? this.options.disabled,
+ inputs: options.inputs ?? this.options.inputs,
+ output: options.output ?? this.options.output,
+ configSchema: options.configSchema ?? this.options.configSchema, // TODO: some config merging or smth
+ factory: ({ node, config, inputs }) => {
+ if (options.factory) {
+ return options.factory(
+ {
+ node,
+ config,
+ inputs,
+ orignalFactory: (
+ innerOptions?: {
+ node?: AppNode;
+ config?: TConfig;
+ inputs?: Expand>;
+ },
+ innerProps?: TProps,
+ ) =>
+ this.options.factory(
+ {
+ node: innerOptions?.node ?? node,
+ config: innerOptions?.config ?? config,
+ inputs: innerOptions?.inputs ?? inputs,
+ },
+ innerProps ?? options.props,
+ ),
+ },
+ options.props,
+ );
+ }
+
+ return this.options.factory(
+ {
+ node,
+ config,
+ inputs,
+ },
+ options.props,
+ );
+ },
+ });
+ }
+}
+
+/**
+ * A simpler replacement for wrapping up `createExtension` inside a kind or type. This allows for a cleaner API for creating
+ * types and instances of those types.
+ */
+export function createExtensionKind<
+ TProps,
+ TInputs extends AnyExtensionInputMap,
+ TOutput extends AnyExtensionDataMap,
+ TConfig,
+>(options: ExtensionKindOptions) {
+ return ExtensionKind.create(options);
+}