core-api: add plugin.provide and extensions + core extension functions

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: blam <ben@blam.sh>
This commit is contained in:
Patrik Oldsberg
2020-11-25 17:37:47 +01:00
parent 0bdeeb20cf
commit 18821642ea
4 changed files with 147 additions and 2 deletions
@@ -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 = <ExtensionComponent />;
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 = <ExtensionComponent1 />;
const element2 = <ExtensionComponent2 />;
expect(getComponentData(element1, 'core.plugin')).toBe(plugin);
expect(getComponentData(element2, 'core.plugin')).toBe(plugin);
expect(getComponentData(element2, 'core.mountPoint')).toBe(routeRef);
});
});
+59
View File
@@ -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<Props extends {}>(options: {
component: ComponentType<Props>;
mountPoint: RouteRef;
}): Extension<ExoticComponent<Props>> {
const { component, mountPoint } = options;
return createReactExtension({
component,
data: {
'core.mountPoint': mountPoint,
},
});
}
export function createComponentExtension<Props extends {}>(options: {
component: ComponentType<Props>;
}): Extension<ExoticComponent<Props>> {
const { component } = options;
return createReactExtension({ component });
}
export function createReactExtension<Props extends {}>(options: {
component: ComponentType<Props>;
data?: Record<string, unknown>;
}): Extension<ExoticComponent<Props>> {
const { component: Component, data = {} } = options;
return {
expose(plugin: BackstagePlugin): ExoticComponent<Props> {
const Result = (props: Props) => <Component {...props} />;
attachComponentData(Result, 'core.plugin', plugin);
for (const [key, value] of Object.entries(data)) {
attachComponentData(Result, key, value);
}
return Result as ExoticComponent<Props>;
},
};
}
+11 -2
View File
@@ -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<T>(extension: Extension<T>): T {
return extension.expose(this);
}
toString() {
return `plugin{${this.config.id}}`;
}
+5
View File
@@ -66,10 +66,15 @@ export type PluginOutput =
| RedirectRouteOutput
| FeatureFlagOutput;
export type Extension<T> = {
expose(plugin: BackstagePlugin): T;
};
export type BackstagePlugin = {
getId(): string;
output(): PluginOutput[];
getApis(): Iterable<AnyApiFactory>;
provide<T>(extension: Extension<T>): T;
};
export type PluginConfig = {