diff --git a/packages/core-api/src/lib/extensions.test.tsx b/packages/core-api/src/lib/extensions.test.tsx
new file mode 100644
index 0000000000..92fcecebc4
--- /dev/null
+++ b/packages/core-api/src/lib/extensions.test.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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 { createPlugin } from '../plugin';
+import { createRouteRef } from '../routing';
+import { getComponentData } from './componentData';
+import {
+ createComponentExtension,
+ createReactExtension,
+ createRoutableExtension,
+} from './extensions';
+
+const plugin = createPlugin({
+ id: 'my-plugin',
+});
+
+describe('extensions', () => {
+ it('should create a react extension with component data', () => {
+ const Component = () => null;
+
+ const extension = createReactExtension({
+ component: Component,
+ data: {
+ myData: { foo: 'bar' },
+ },
+ });
+
+ const ExtensionComponent = plugin.provide(extension);
+ const element = ;
+
+ expect(getComponentData(element, 'core.plugin')).toBe(plugin);
+ expect(getComponentData(element, 'myData')).toEqual({ foo: 'bar' });
+ });
+
+ it('should create react extensions of different types', () => {
+ const Component = () => null;
+ const routeRef = createRouteRef({ path: '/foo', title: 'Foo' });
+
+ const extension1 = createComponentExtension({
+ component: Component,
+ });
+
+ const extension2 = createRoutableExtension({
+ component: Component,
+ mountPoint: routeRef,
+ });
+
+ const ExtensionComponent1 = plugin.provide(extension1);
+ const ExtensionComponent2 = plugin.provide(extension2);
+
+ const element1 = ;
+ const element2 = ;
+
+ expect(getComponentData(element1, 'core.plugin')).toBe(plugin);
+ expect(getComponentData(element2, 'core.plugin')).toBe(plugin);
+ expect(getComponentData(element2, 'core.mountPoint')).toBe(routeRef);
+ });
+});
diff --git a/packages/core-api/src/lib/extensions.tsx b/packages/core-api/src/lib/extensions.tsx
new file mode 100644
index 0000000000..c0a71a2a90
--- /dev/null
+++ b/packages/core-api/src/lib/extensions.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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, { ExoticComponent, ComponentType } from 'react';
+import { RouteRef } from '../routing';
+import { attachComponentData } from './componentData';
+import { Extension, BackstagePlugin } from '../plugin/types';
+
+export function createRoutableExtension(options: {
+ component: ComponentType;
+ mountPoint: RouteRef;
+}): Extension> {
+ const { component, mountPoint } = options;
+ return createReactExtension({
+ component,
+ data: {
+ 'core.mountPoint': mountPoint,
+ },
+ });
+}
+
+export function createComponentExtension(options: {
+ component: ComponentType;
+}): Extension> {
+ const { component } = options;
+ return createReactExtension({ component });
+}
+
+export function createReactExtension(options: {
+ component: ComponentType;
+ data?: Record;
+}): Extension> {
+ const { component: Component, data = {} } = options;
+ return {
+ expose(plugin: BackstagePlugin): ExoticComponent {
+ const Result = (props: Props) => ;
+
+ attachComponentData(Result, 'core.plugin', plugin);
+ for (const [key, value] of Object.entries(data)) {
+ attachComponentData(Result, key, value);
+ }
+
+ return Result as ExoticComponent;
+ },
+ };
+}
diff --git a/packages/core-api/src/plugin/Plugin.tsx b/packages/core-api/src/plugin/Plugin.tsx
index d69bb7e721..f477674f4d 100644
--- a/packages/core-api/src/plugin/Plugin.tsx
+++ b/packages/core-api/src/plugin/Plugin.tsx
@@ -14,10 +14,15 @@
* limitations under the License.
*/
-import { PluginConfig, PluginOutput, BackstagePlugin } from './types';
+import {
+ PluginConfig,
+ PluginOutput,
+ BackstagePlugin,
+ Extension,
+} from './types';
import { AnyApiFactory } from '../apis';
-export class PluginImpl {
+export class PluginImpl implements BackstagePlugin {
private storedOutput?: PluginOutput[];
constructor(private readonly config: PluginConfig) {}
@@ -65,6 +70,10 @@ export class PluginImpl {
return this.storedOutput;
}
+ provide(extension: Extension): T {
+ return extension.expose(this);
+ }
+
toString() {
return `plugin{${this.config.id}}`;
}
diff --git a/packages/core-api/src/plugin/types.ts b/packages/core-api/src/plugin/types.ts
index 855264c5f4..dd19948a0a 100644
--- a/packages/core-api/src/plugin/types.ts
+++ b/packages/core-api/src/plugin/types.ts
@@ -66,10 +66,15 @@ export type PluginOutput =
| RedirectRouteOutput
| FeatureFlagOutput;
+export type Extension = {
+ expose(plugin: BackstagePlugin): T;
+};
+
export type BackstagePlugin = {
getId(): string;
output(): PluginOutput[];
getApis(): Iterable;
+ provide(extension: Extension): T;
};
export type PluginConfig = {