From f933c294beea9a56459a40f53625f126e0931042 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 10 Jul 2019 17:30:59 -0500 Subject: [PATCH] Add support for plugin-scoped plugin contracts --- .../core/public/kibana-plugin-public.md | 3 +- .../public/kibana-plugin-public.plugin.md | 2 +- .../kibana-plugin-public.plugin.setup.md | 4 +- .../kibana-plugin-public.plugin.start.md | 4 +- ...a-plugin-public.pluginlifecyclecontract.md | 13 ++++ .../core/server/kibana-plugin-server.md | 3 +- .../server/kibana-plugin-server.plugin.md | 2 +- .../kibana-plugin-server.plugin.setup.md | 4 +- .../kibana-plugin-server.plugin.start.md | 4 +- ...a-plugin-server.pluginlifecyclecontract.md | 13 ++++ src/core/public/index.ts | 8 +- src/core/public/plugins/index.ts | 2 +- src/core/public/plugins/plugin.ts | 21 ++++- .../public/plugins/plugins_service.test.ts | 77 ++++++++++++++++++- src/core/public/plugins/plugins_service.ts | 20 +++-- src/core/public/public.api.md | 7 +- src/core/server/index.ts | 1 + src/core/server/plugins/index.ts | 1 + src/core/server/plugins/plugin.ts | 21 ++++- .../server/plugins/plugins_system.test.ts | 26 +++++++ src/core/server/plugins/plugins_system.ts | 26 +++++-- src/core/server/server.api.md | 7 +- src/core/types/core_service.ts | 2 + 23 files changed, 235 insertions(+), 36 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.pluginlifecyclecontract.md create mode 100644 docs/development/core/server/kibana-plugin-server.pluginlifecyclecontract.md diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 98b6a8703f54358..c88b070e5b0c2ae 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -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 PluginInitializer. | +| [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 PluginInitializer | | [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | | @@ -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 plugin export at the root of a plugin's public 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) | diff --git a/docs/development/core/public/kibana-plugin-public.plugin.md b/docs/development/core/public/kibana-plugin-public.plugin.md index 879897ec18d8479..64dce946cf696d0 100644 --- a/docs/development/core/public/kibana-plugin-public.plugin.md +++ b/docs/development/core/public/kibana-plugin-public.plugin.md @@ -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). Signature: diff --git a/docs/development/core/public/kibana-plugin-public.plugin.setup.md b/docs/development/core/public/kibana-plugin-public.plugin.setup.md index 56855b02cfbad4a..18ea89adf63d977 100644 --- a/docs/development/core/public/kibana-plugin-public.plugin.setup.md +++ b/docs/development/core/public/kibana-plugin-public.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): PluginLifecycleContract | Promise>; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; Returns: -`TSetup | Promise` +`PluginLifecycleContract | Promise>` diff --git a/docs/development/core/public/kibana-plugin-public.plugin.start.md b/docs/development/core/public/kibana-plugin-public.plugin.start.md index b132706f4b7c02e..b873bba963c358b 100644 --- a/docs/development/core/public/kibana-plugin-public.plugin.start.md +++ b/docs/development/core/public/kibana-plugin-public.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): PluginLifecycleContract | Promise>; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`PluginLifecycleContract | Promise>` diff --git a/docs/development/core/public/kibana-plugin-public.pluginlifecyclecontract.md b/docs/development/core/public/kibana-plugin-public.pluginlifecyclecontract.md new file mode 100644 index 000000000000000..d4080d350a45f40 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.pluginlifecyclecontract.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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. + +Signature: + +```typescript +export declare type PluginLifecycleContract = T extends (dependencyId: symbol) => infer U ? U : T; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 191819a3e49249f..d531f3d34110817 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -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 PluginInitializer. | +| [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) | | @@ -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 plugin export at the root of a plugin's server 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. | diff --git a/docs/development/core/server/kibana-plugin-server.plugin.md b/docs/development/core/server/kibana-plugin-server.plugin.md index 5cef833ecc30e47..dbe49f8529144c4 100644 --- a/docs/development/core/server/kibana-plugin-server.plugin.md +++ b/docs/development/core/server/kibana-plugin-server.plugin.md @@ -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). Signature: diff --git a/docs/development/core/server/kibana-plugin-server.plugin.setup.md b/docs/development/core/server/kibana-plugin-server.plugin.setup.md index 5ceb504f796f18a..5f06146372ea52d 100644 --- a/docs/development/core/server/kibana-plugin-server.plugin.setup.md +++ b/docs/development/core/server/kibana-plugin-server.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): PluginLifecycleContract | Promise>; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; Returns: -`TSetup | Promise` +`PluginLifecycleContract | Promise>` diff --git a/docs/development/core/server/kibana-plugin-server.plugin.start.md b/docs/development/core/server/kibana-plugin-server.plugin.start.md index 6ce9f05de77311b..973d6db2f41fc3f 100644 --- a/docs/development/core/server/kibana-plugin-server.plugin.start.md +++ b/docs/development/core/server/kibana-plugin-server.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): PluginLifecycleContract | Promise>; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`PluginLifecycleContract | Promise>` diff --git a/docs/development/core/server/kibana-plugin-server.pluginlifecyclecontract.md b/docs/development/core/server/kibana-plugin-server.pluginlifecyclecontract.md new file mode 100644 index 000000000000000..be2dbd050eda978 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginlifecyclecontract.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [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. + +Signature: + +```typescript +export declare type PluginLifecycleContract = T extends (dependencyId: symbol) => infer U ? U : T; +``` diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 2f3140d3ec55bfc..5090352885d8a63 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -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'; @@ -163,6 +168,7 @@ export { Plugin, PluginInitializer, PluginInitializerContext, + PluginLifecycleContract, Toast, ToastInput, ToastsApi, diff --git a/src/core/public/plugins/index.ts b/src/core/public/plugins/index.ts index fc16b6b004565e5..5619756339063fa 100644 --- a/src/core/public/plugins/index.ts +++ b/src/core/public/plugins/index.ts @@ -18,5 +18,5 @@ */ export * from './plugins_service'; -export { Plugin, PluginInitializer } from './plugin'; +export { Plugin, PluginInitializer, PluginLifecycleContract } from './plugin'; export { PluginInitializerContext } from './plugin_context'; diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index 5d40086336cdfa1..22d52ad1a87e5a0 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -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 extends (dependencyId: symbol) => infer U ? U : T; + +/** + * The interface that should be returned by a {@link PluginInitializer}. * * @public */ @@ -33,8 +41,14 @@ export interface Plugin< TPluginsSetup extends {} = {}, TPluginsStart extends {} = {} > { - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + setup( + core: CoreSetup, + plugins: TPluginsSetup + ): PluginLifecycleContract | Promise>; + start( + core: CoreStart, + plugins: TPluginsStart + ): PluginLifecycleContract | Promise>; stop?(): void; } @@ -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; private instance?: Plugin; diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 70ff9ff1e851b3e..8eb7dba065b121e 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -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(), @@ -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(() => '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); @@ -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(), @@ -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(() => '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); diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 6c006c860883c2a..7e1ea37081d7462 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -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'; @@ -73,7 +73,7 @@ export class PluginsService implements CoreService(); + const contracts = new Map>(); for (const [pluginName, plugin] of this.plugins.entries()) { const pluginDeps = new Set([ ...plugin.requiredPlugins, @@ -85,7 +85,12 @@ export class PluginsService implements CoreService(); + const contracts = new Map>(); for (const [pluginName, plugin] of this.plugins.entries()) { const pluginDeps = new Set([ ...plugin.requiredPlugins, @@ -122,7 +127,12 @@ export class PluginsService implements CoreService { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): PluginLifecycleContract | Promise>; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): PluginLifecycleContract | Promise>; // (undocumented) stop?(): void; } @@ -487,6 +487,9 @@ export type PluginInitializer = 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) diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 1e783eb37e77d55..fefecfe5cafbd64 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -76,6 +76,7 @@ export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; export { DiscoveredPlugin, Plugin, + PluginLifecycleContract, PluginInitializer, PluginInitializerContext, PluginName, diff --git a/src/core/server/plugins/index.ts b/src/core/server/plugins/index.ts index c2e66cbb0bb7d0e..efeec3481e2056a 100644 --- a/src/core/server/plugins/index.ts +++ b/src/core/server/plugins/index.ts @@ -26,6 +26,7 @@ export { DiscoveredPlugin, DiscoveredPluginInternal, Plugin, + PluginLifecycleContract, PluginInitializer, PluginName, } from './plugin'; diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 3f24d44992b37d0..636264d6fdee9df 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -131,7 +131,15 @@ export interface DiscoveredPluginInternal extends DiscoveredPlugin { } /** - * 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 extends (dependencyId: symbol) => infer U ? U : T; + +/** + * The interface that should be returned by a {@link PluginInitializer}. * * @public */ @@ -141,8 +149,14 @@ export interface Plugin< TPluginsSetup extends {} = {}, TPluginsStart extends {} = {} > { - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + setup( + core: CoreSetup, + plugins: TPluginsSetup + ): PluginLifecycleContract | Promise>; + start( + core: CoreStart, + plugins: TPluginsStart + ): PluginLifecycleContract | Promise>; stop?(): void; } @@ -177,6 +191,7 @@ export class PluginWrapper< public readonly optionalPlugins: PluginManifest['optionalPlugins']; public readonly includesServerPlugin: PluginManifest['server']; public readonly includesUiPlugin: PluginManifest['ui']; + public readonly opaqueId = Symbol(); private readonly log: Logger; diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index d7435ffdfe282e1..561bf9c720d5d83 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -132,6 +132,32 @@ Array [ `); }); +test(`calls scoped plugin function for dependencies`, async () => { + mockCreatePluginSetupContext.mockReturnValue({}); + mockCreatePluginStartContext.mockReturnValue({}); + + const plugin1 = createPlugin('plugin-1'); + const scopedSetup = jest.fn(() => 'plugin-1-setup'); + const scopedStart = jest.fn(() => 'plugin-1-start'); + jest.spyOn(plugin1, 'setup').mockResolvedValue(scopedSetup); + jest.spyOn(plugin1, 'start').mockResolvedValue(scopedStart); + + const plugin2 = createPlugin('plugin-2', { required: ['plugin-1'] }); + const plugin2Setup = jest.spyOn(plugin2, 'setup').mockResolvedValue('plugin-2-setup'); + const plugin2Start = jest.spyOn(plugin2, 'start').mockResolvedValue('plugin-2-start'); + + pluginsSystem.addPlugin(plugin1); + pluginsSystem.addPlugin(plugin2); + + await pluginsSystem.setupPlugins(setupDeps); + await pluginsSystem.startPlugins({}); + + expect(scopedSetup).toHaveBeenCalledWith(plugin2.opaqueId); + expect(scopedStart).toHaveBeenCalledWith(plugin2.opaqueId); + expect(plugin2Setup).toHaveBeenCalledWith({}, { 'plugin-1': 'plugin-1-setup' }); + expect(plugin2Start).toHaveBeenCalledWith({}, { 'plugin-1': 'plugin-1-start' }); +}); + test('correctly orders plugins and returns exposed values for "setup" and "start"', async () => { interface Contracts { setup: Record; diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 37eab8226af7286..da72d79704e9580 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -21,7 +21,13 @@ import { pick } from 'lodash'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginWrapper, PluginName } from './plugin'; +import { + DiscoveredPlugin, + DiscoveredPluginInternal, + PluginWrapper, + PluginName, + PluginLifecycleContract, +} from './plugin'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; @@ -41,7 +47,7 @@ export class PluginsSystem { } public async setupPlugins(deps: PluginsServiceSetupDeps) { - const contracts = new Map(); + const contracts = new Map>(); if (this.plugins.size === 0) { return contracts; } @@ -62,7 +68,12 @@ export class PluginsSystem { // Only set if present. Could be absent if plugin does not have server-side code or is a // missing optional dependency. 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; @@ -85,7 +96,7 @@ export class PluginsSystem { } public async startPlugins(deps: PluginsServiceStartDeps) { - const contracts = new Map(); + const contracts = new Map>(); if (this.satupPlugins.length === 0) { return contracts; } @@ -101,7 +112,12 @@ export class PluginsSystem { // Only set if present. Could be absent if plugin does not have server-side code or is a // missing optional dependency. 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; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 90da0df5679136f..5998cf1735efaa7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -317,9 +317,9 @@ export interface OnPreAuthToolkit { // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): PluginLifecycleContract | Promise>; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): PluginLifecycleContract | Promise>; // (undocumented) stop?(): void; } @@ -342,6 +342,9 @@ export interface PluginInitializerContext { logger: LoggerFactory; } +// @public +export type PluginLifecycleContract = T extends (dependencyId: symbol) => infer U ? U : T; + // @public export type PluginName = string; diff --git a/src/core/types/core_service.ts b/src/core/types/core_service.ts index 438cac2a50577c7..1e5a857cd664e37 100644 --- a/src/core/types/core_service.ts +++ b/src/core/types/core_service.ts @@ -23,6 +23,8 @@ export interface PluginScoped { forPlugin(plugin: PluginName): T; } +export type LifecycleContract = T extends PluginScoped ? U : T; + export type LifecycleReturnType = T | PluginScoped | Promise>; /** @internal */