Skip to content

Commit

Permalink
Add support for plugin-scoped plugin contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdover committed Jul 11, 2019
1 parent 0788c0d commit f933c29
Show file tree
Hide file tree
Showing 23 changed files with 235 additions and 36 deletions.
3 changes: 2 additions & 1 deletion docs/development/core/public/kibana-plugin-public.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | |
| [OverlayRef](./kibana-plugin-public.overlayref.md) | |
| [OverlayStart](./kibana-plugin-public.overlaystart.md) | |
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a [PluginInitializer](./kibana-plugin-public.plugininitializer.md)<!-- -->. |
| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a <code>PluginInitializer</code> |
| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | |

Expand All @@ -61,6 +61,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
| [HttpStart](./kibana-plugin-public.httpstart.md) | |
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
| [PluginLifecycleContract](./kibana-plugin-public.pluginlifecyclecontract.md) | A plugin contact can either be a raw value or a function that receives a unique symbol per dependency to provide a pre-configured or scoped contract to the dependency. |
| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | |
| [ToastInput](./kibana-plugin-public.toastinput.md) | |
| [UiSettingsClientContract](./kibana-plugin-public.uisettingsclientcontract.md) | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Plugin interface

The interface that should be returned by a `PluginInitializer`<!-- -->.
The interface that should be returned by a [PluginInitializer](./kibana-plugin-public.plugininitializer.md)<!-- -->.

<b>Signature:</b>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<b>Signature:</b>

```typescript
setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
setup(core: CoreSetup, plugins: TPluginsSetup): PluginLifecycleContract<TSetup> | Promise<PluginLifecycleContract<TSetup>>;
```

## Parameters
Expand All @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;

<b>Returns:</b>

`TSetup | Promise<TSetup>`
`PluginLifecycleContract<TSetup> | Promise<PluginLifecycleContract<TSetup>>`

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<b>Signature:</b>

```typescript
start(core: CoreStart, plugins: TPluginsStart): TStart | Promise<TStart>;
start(core: CoreStart, plugins: TPluginsStart): PluginLifecycleContract<TStart> | Promise<PluginLifecycleContract<TStart>>;
```

## Parameters
Expand All @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise<TStart>;

<b>Returns:</b>

`TStart | Promise<TStart>`
`PluginLifecycleContract<TStart> | Promise<PluginLifecycleContract<TStart>>`

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [PluginLifecycleContract](./kibana-plugin-public.pluginlifecyclecontract.md)

## PluginLifecycleContract type

A plugin contact can either be a raw value or a function that receives a unique symbol per dependency to provide a pre-configured or scoped contract to the dependency.

<b>Signature:</b>

```typescript
export declare type PluginLifecycleContract<T> = T extends (dependencyId: symbol) => infer U ? U : T;
```
3 changes: 2 additions & 1 deletion docs/development/core/server/kibana-plugin-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata |
| [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. |
| [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. |
| [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
| [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a [PluginInitializer](./kibana-plugin-server.plugininitializer.md)<!-- -->. |
| [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. |
| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | |
| [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | |
Expand Down Expand Up @@ -81,6 +81,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | |
| [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | |
| [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>server</code> directory should conform to this interface. |
| [PluginLifecycleContract](./kibana-plugin-server.pluginlifecyclecontract.md) | A plugin contact can either be a raw value or a function that receives a unique symbol per dependency to provide a pre-configured or scoped contract to the dependency. |
| [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. |
| [RecursiveReadonly](./kibana-plugin-server.recursivereadonly.md) | |
| [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Plugin interface

The interface that should be returned by a `PluginInitializer`<!-- -->.
The interface that should be returned by a [PluginInitializer](./kibana-plugin-server.plugininitializer.md)<!-- -->.

<b>Signature:</b>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<b>Signature:</b>

```typescript
setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
setup(core: CoreSetup, plugins: TPluginsSetup): PluginLifecycleContract<TSetup> | Promise<PluginLifecycleContract<TSetup>>;
```

## Parameters
Expand All @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;

<b>Returns:</b>

`TSetup | Promise<TSetup>`
`PluginLifecycleContract<TSetup> | Promise<PluginLifecycleContract<TSetup>>`

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<b>Signature:</b>

```typescript
start(core: CoreStart, plugins: TPluginsStart): TStart | Promise<TStart>;
start(core: CoreStart, plugins: TPluginsStart): PluginLifecycleContract<TStart> | Promise<PluginLifecycleContract<TStart>>;
```

## Parameters
Expand All @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise<TStart>;

<b>Returns:</b>

`TStart | Promise<TStart>`
`PluginLifecycleContract<TStart> | Promise<PluginLifecycleContract<TStart>>`

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [PluginLifecycleContract](./kibana-plugin-server.pluginlifecyclecontract.md)

## PluginLifecycleContract type

A plugin contact can either be a raw value or a function that receives a unique symbol per dependency to provide a pre-configured or scoped contract to the dependency.

<b>Signature:</b>

```typescript
export declare type PluginLifecycleContract<T> = T extends (dependencyId: symbol) => infer U ? U : T;
```
8 changes: 7 additions & 1 deletion src/core/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ import {
ToastsApi,
} from './notifications';
import { OverlayRef, OverlayStart } from './overlays';
import { Plugin, PluginInitializer, PluginInitializerContext } from './plugins';
import {
Plugin,
PluginInitializer,
PluginInitializerContext,
PluginLifecycleContract,
} from './plugins';
import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './ui_settings';
import { ApplicationSetup, Capabilities, ApplicationStart } from './application';
import { DocLinksStart } from './doc_links';
Expand Down Expand Up @@ -163,6 +168,7 @@ export {
Plugin,
PluginInitializer,
PluginInitializerContext,
PluginLifecycleContract,
Toast,
ToastInput,
ToastsApi,
Expand Down
2 changes: 1 addition & 1 deletion src/core/public/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
*/

export * from './plugins_service';
export { Plugin, PluginInitializer } from './plugin';
export { Plugin, PluginInitializer, PluginLifecycleContract } from './plugin';
export { PluginInitializerContext } from './plugin_context';
21 changes: 18 additions & 3 deletions src/core/public/plugins/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ import { loadPluginBundle } from './plugin_loader';
import { CoreStart, CoreSetup } from '..';

/**
* The interface that should be returned by a `PluginInitializer`.
* A plugin contact can either be a raw value or a function that receives a unique symbol per dependency to provide
* a pre-configured or scoped contract to the dependency.
*
* @public
*/
export type PluginLifecycleContract<T> = T extends (dependencyId: symbol) => infer U ? U : T;

/**
* The interface that should be returned by a {@link PluginInitializer}.
*
* @public
*/
Expand All @@ -33,8 +41,14 @@ export interface Plugin<
TPluginsSetup extends {} = {},
TPluginsStart extends {} = {}
> {
setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
start(core: CoreStart, plugins: TPluginsStart): TStart | Promise<TStart>;
setup(
core: CoreSetup,
plugins: TPluginsSetup
): PluginLifecycleContract<TSetup> | Promise<PluginLifecycleContract<TSetup>>;
start(
core: CoreStart,
plugins: TPluginsStart
): PluginLifecycleContract<TStart> | Promise<PluginLifecycleContract<TStart>>;
stop?(): void;
}

Expand Down Expand Up @@ -67,6 +81,7 @@ export class PluginWrapper<
public readonly configPath: DiscoveredPlugin['configPath'];
public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins'];
public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins'];
public readonly opaqueId = Symbol();
private initializer?: PluginInitializer<TSetup, TStart, TPluginsSetup, TPluginsStart>;
private instance?: Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;

Expand Down
77 changes: 75 additions & 2 deletions src/core/public/plugins/plugins_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ test('`PluginsService.setup` exposes dependent setup contracts to plugins', asyn

test('`PluginsService.setup` does not set missing dependent setup contracts', async () => {
mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([
{ id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) },
{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) },
]);
mockPluginInitializers.set('pluginD', jest.fn(() => ({
setup: jest.fn(),
Expand All @@ -226,6 +226,42 @@ test('`PluginsService.setup` returns plugin setup contracts', async () => {
expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(2);
});

test('`PluginService.setup` calls scoped plugin function for dependencies', async () => {
const setupScoped = jest.fn<string, [symbol]>(() => 'plugin-d-setup');

mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([
{ id: 'pluginD', plugin: createManifest('pluginD') },
{ id: 'pluginE', plugin: createManifest('pluginE', { required: ['pluginD'] }) },
]);

mockPluginInitializers
.set(
'pluginD',
jest.fn(
() =>
({
setup: jest.fn(() => setupScoped),
start: jest.fn(),
} as any)
)
)
.set('pluginE', jest.fn(() => ({
setup: jest.fn(),
start: jest.fn(),
})) as any);

const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockSetupDeps);

expect(setupScoped).toHaveBeenCalled();
expect(typeof setupScoped.mock.calls[0][0] === 'symbol').toBe(true);

const pluginEInstance = mockPluginInitializers.get('pluginE')!.mock.results[0].value;
expect(pluginEInstance.setup).toHaveBeenCalledWith(mockSetupContext, {
pluginD: 'plugin-d-setup',
});
});

test('`PluginsService.start` exposes dependent start contracts to plugins', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockSetupDeps);
Expand All @@ -247,7 +283,7 @@ test('`PluginsService.start` exposes dependent start contracts to plugins', asyn

test('`PluginsService.start` does not set missing dependent start contracts', async () => {
mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([
{ id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) },
{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) },
]);
mockPluginInitializers.set('pluginD', jest.fn(() => ({
setup: jest.fn(),
Expand Down Expand Up @@ -275,6 +311,43 @@ test('`PluginsService.start` returns plugin start contracts', async () => {
expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(3);
});

test('`PluginService.start` calls scoped plugin function for dependencies', async () => {
const startScoped = jest.fn<string, [symbol]>(() => 'plugin-d-start');

mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([
{ id: 'pluginD', plugin: createManifest('pluginD') },
{ id: 'pluginE', plugin: createManifest('pluginE', { required: ['pluginD'] }) },
]);

mockPluginInitializers
.set(
'pluginD',
jest.fn(
() =>
({
setup: jest.fn(),
start: jest.fn(() => startScoped),
} as any)
)
)
.set('pluginE', jest.fn(() => ({
setup: jest.fn(),
start: jest.fn(),
})) as any);

const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockSetupDeps);
await pluginsService.start(mockStartDeps);

expect(startScoped).toHaveBeenCalled();
expect(typeof startScoped.mock.calls[0][0] === 'symbol').toBe(true);

const pluginEInstance = mockPluginInitializers.get('pluginE')!.mock.results[0].value;
expect(pluginEInstance.start).toHaveBeenCalledWith(mockStartContext, {
pluginD: 'plugin-d-start',
});
});

test('`PluginService.stop` calls the stop function on each plugin', async () => {
const pluginsService = new PluginsService(mockCoreContext);
await pluginsService.setup(mockSetupDeps);
Expand Down
20 changes: 15 additions & 5 deletions src/core/public/plugins/plugins_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import { PluginName } from '../../server';
import { PluginName, PluginLifecycleContract } from '../../server';
import { CoreService } from '../../types';
import { CoreContext, InternalCoreStart } from '../core_system';
import { PluginWrapper } from './plugin';
Expand Down Expand Up @@ -73,7 +73,7 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
await this.loadPluginBundles(deps.http.basePath.prepend);

// Setup each plugin with required and optional plugin contracts
const contracts = new Map<string, unknown>();
const contracts = new Map<string, PluginLifecycleContract<unknown>>();
for (const [pluginName, plugin] of this.plugins.entries()) {
const pluginDeps = new Set([
...plugin.requiredPlugins,
Expand All @@ -85,7 +85,12 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
// Only set if present. Could be absent if plugin does not have client-side code or is a
// missing optional plugin.
if (contracts.has(dependencyName)) {
depContracts[dependencyName] = contracts.get(dependencyName);
const contract = contracts.get(dependencyName);
if (typeof contract === 'function') {
depContracts[dependencyName] = contract(plugin.opaqueId);
} else {
depContracts[dependencyName] = contract;
}
}

return depContracts;
Expand All @@ -110,7 +115,7 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS

public async start(deps: PluginsServiceStartDeps) {
// Setup each plugin with required and optional plugin contracts
const contracts = new Map<string, unknown>();
const contracts = new Map<string, PluginLifecycleContract<unknown>>();
for (const [pluginName, plugin] of this.plugins.entries()) {
const pluginDeps = new Set([
...plugin.requiredPlugins,
Expand All @@ -122,7 +127,12 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
// Only set if present. Could be absent if plugin does not have client-side code or is a
// missing optional plugin.
if (contracts.has(dependencyName)) {
depContracts[dependencyName] = contracts.get(dependencyName);
const contract = contracts.get(dependencyName);
if (typeof contract === 'function') {
depContracts[dependencyName] = contract(plugin.opaqueId);
} else {
depContracts[dependencyName] = contract;
}
}

return depContracts;
Expand Down
7 changes: 5 additions & 2 deletions src/core/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,9 @@ export interface OverlayStart {
// @public
export interface Plugin<TSetup = void, TStart = void, TPluginsSetup extends {} = {}, TPluginsStart extends {} = {}> {
// (undocumented)
setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
setup(core: CoreSetup, plugins: TPluginsSetup): PluginLifecycleContract<TSetup> | Promise<PluginLifecycleContract<TSetup>>;
// (undocumented)
start(core: CoreStart, plugins: TPluginsStart): TStart | Promise<TStart>;
start(core: CoreStart, plugins: TPluginsStart): PluginLifecycleContract<TStart> | Promise<PluginLifecycleContract<TStart>>;
// (undocumented)
stop?(): void;
}
Expand All @@ -487,6 +487,9 @@ export type PluginInitializer<TSetup, TStart, TPluginsSetup extends Record<strin
export interface PluginInitializerContext {
}

// @public
export type PluginLifecycleContract<T> = T extends (dependencyId: symbol) => infer U ? U : T;

// Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
Expand Down
Loading

0 comments on commit f933c29

Please sign in to comment.