Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

async-import plugins in the server side #170856

Merged
merged 16 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions examples/bfetch_explorer/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Side Public License, v 1.
*/

import { BfetchExplorerPlugin } from './plugin';

export const plugin = () => new BfetchExplorerPlugin();
export const plugin = async () => {
const { BfetchExplorerPlugin } = await import('./plugin');
return new BfetchExplorerPlugin();
};
4 changes: 2 additions & 2 deletions examples/content_management_examples/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
*/

import { PluginInitializerContext } from '@kbn/core/server';
import { ContentManagementExamplesPlugin } from './plugin';

export function plugin(initializerContext: PluginInitializerContext) {
export async function plugin(initializerContext: PluginInitializerContext) {
const { ContentManagementExamplesPlugin } = await import('./plugin');
return new ContentManagementExamplesPlugin(initializerContext);
}
7 changes: 4 additions & 3 deletions examples/embeddable_examples/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { PluginInitializer } from '@kbn/core/server';

import { EmbeddableExamplesPlugin } from './plugin';

export const plugin: PluginInitializer<void, void> = () => new EmbeddableExamplesPlugin();
export const plugin: PluginInitializer<void, void> = async () => {
const { EmbeddableExamplesPlugin } = await import('./plugin');
return new EmbeddableExamplesPlugin();
};
6 changes: 4 additions & 2 deletions examples/feature_control_examples/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* Side Public License, v 1.
*/
import { PluginInitializer } from '@kbn/core/server';
import { FeatureControlsPluginExample } from './plugin';

export const plugin: PluginInitializer<void, void> = () => new FeatureControlsPluginExample();
export const plugin: PluginInitializer<void, void> = async () => {
const { FeatureControlsPluginExample } = await import('./plugin');
return new FeatureControlsPluginExample();
};
5 changes: 2 additions & 3 deletions examples/field_formats_example/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
* Side Public License, v 1.
*/

import { FieldFormatsExamplePlugin } from './plugin';

export function plugin() {
export async function plugin() {
const { FieldFormatsExamplePlugin } = await import('./plugin');
return new FieldFormatsExamplePlugin();
}
4 changes: 2 additions & 2 deletions examples/files_example/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
*/

import { PluginInitializerContext } from '@kbn/core/server';
import { FilesExamplePlugin } from './plugin';

// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.

export function plugin(initializerContext: PluginInitializerContext) {
export async function plugin(initializerContext: PluginInitializerContext) {
const { FilesExamplePlugin } = await import('./plugin');
return new FilesExamplePlugin(initializerContext);
}
6 changes: 4 additions & 2 deletions examples/guided_onboarding_example/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/

import { PluginInitializerContext } from '@kbn/core/server';
import { GuidedOnboardingExamplePlugin } from './plugin';

export const plugin = (ctx: PluginInitializerContext) => new GuidedOnboardingExamplePlugin(ctx);
export const plugin = async (ctx: PluginInitializerContext) => {
const { GuidedOnboardingExamplePlugin } = await import('./plugin');
return new GuidedOnboardingExamplePlugin(ctx);
};
6 changes: 4 additions & 2 deletions examples/preboot_example/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import type { TypeOf } from '@kbn/config-schema';
import type { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server';

import { ConfigSchema } from './config';
import { PrebootExamplePlugin } from './plugin';

export const config: PluginConfigDescriptor<TypeOf<typeof ConfigSchema>> = {
schema: ConfigSchema,
exposeToBrowser: { token: true },
};

export const plugin = (context: PluginInitializerContext) => new PrebootExamplePlugin(context);
export const plugin = async (context: PluginInitializerContext) => {
const { PrebootExamplePlugin } = await import('./plugin');
return new PrebootExamplePlugin(context);
};
5 changes: 2 additions & 3 deletions examples/response_stream/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@

import { PluginInitializerContext } from '@kbn/core/server';

import { ResponseStreamPlugin } from './plugin';

export function plugin(initializerContext: PluginInitializerContext) {
export async function plugin(initializerContext: PluginInitializerContext) {
const { ResponseStreamPlugin } = await import('./plugin');
return new ResponseStreamPlugin(initializerContext);
}
7 changes: 4 additions & 3 deletions examples/routing_example/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { PluginInitializer } from '@kbn/core/server';

import { RoutingExamplePlugin } from './plugin';

export const plugin: PluginInitializer<{}, {}> = () => new RoutingExamplePlugin();
export const plugin: PluginInitializer<{}, {}> = async () => {
const { RoutingExamplePlugin } = await import('./plugin');
return new RoutingExamplePlugin();
};
6 changes: 4 additions & 2 deletions examples/screenshot_mode_example/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/

import { PluginInitializerContext } from '@kbn/core/server';
import { ScreenshotModeExamplePlugin } from './plugin';

export const plugin = (ctx: PluginInitializerContext) => new ScreenshotModeExamplePlugin(ctx);
export const plugin = async (ctx: PluginInitializerContext) => {
const { ScreenshotModeExamplePlugin } = await import('./plugin');
return new ScreenshotModeExamplePlugin(ctx);
};
4 changes: 2 additions & 2 deletions examples/search_examples/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
*/

import { PluginInitializerContext } from '@kbn/core/server';
import { SearchExamplesPlugin } from './plugin';

export function plugin(initializerContext: PluginInitializerContext) {
export async function plugin(initializerContext: PluginInitializerContext) {
const { SearchExamplesPlugin } = await import('./plugin');
return new SearchExamplesPlugin(initializerContext);
}

Expand Down
6 changes: 4 additions & 2 deletions examples/user_profile_examples/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { UserProfilesPlugin } from './plugin';

export const plugin = () => new UserProfilesPlugin();
export const plugin = async () => {
const { UserProfilesPlugin } = await import('./plugin');
return new UserProfilesPlugin();
};
4 changes: 2 additions & 2 deletions examples/v8_profiler_examples/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
*/

import { PluginInitializerContext } from '@kbn/core/server';
import { V8ProfilerExamplesPlugin } from './plugin';

export function plugin(initializerContext: PluginInitializerContext) {
export async function plugin(initializerContext: PluginInitializerContext) {
const { V8ProfilerExamplesPlugin } = await import('./plugin');
return new V8ProfilerExamplesPlugin(initializerContext);
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe('`constructor` correctly sets non-external source', () => {
});
});

test('`setup` fails if `plugin` initializer is not exported', () => {
test('`setup` fails if the plugin has not been initialized', () => {
const manifest = createPluginManifest();
const opaqueId = Symbol();
const plugin = new PluginWrapper({
Expand All @@ -176,11 +176,32 @@ test('`setup` fails if `plugin` initializer is not exported', () => {
expect(() =>
plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {})
).toThrowErrorMatchingInlineSnapshot(
`"The plugin is not initialized. Call the init method first."`
);
});

test('`init` fails if `plugin` initializer is not exported', async () => {
const manifest = createPluginManifest();
const opaqueId = Symbol();
const plugin = new PluginWrapper({
path: 'plugin-without-initializer-path',
manifest,
opaqueId,
initializerContext: createPluginInitializerContext({
coreContext,
opaqueId,
manifest,
instanceInfo,
nodeInfo,
}),
});

await expect(() => plugin.init()).rejects.toThrowErrorMatchingInlineSnapshot(
`"Plugin \\"some-plugin-id\\" does not export \\"plugin\\" definition (plugin-without-initializer-path)."`
);
});

test('`setup` fails if plugin initializer is not a function', () => {
test('`init` fails if plugin initializer is not a function', async () => {
const manifest = createPluginManifest();
const opaqueId = Symbol();
const plugin = new PluginWrapper({
Expand All @@ -196,14 +217,12 @@ test('`setup` fails if plugin initializer is not a function', () => {
}),
});

expect(() =>
plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {})
).toThrowErrorMatchingInlineSnapshot(
await expect(() => plugin.init()).rejects.toThrowErrorMatchingInlineSnapshot(
`"Definition of plugin \\"some-plugin-id\\" should be a function (plugin-with-wrong-initializer-path)."`
);
});

test('`setup` fails if initializer does not return object', () => {
test('`init` fails if initializer does not return object', async () => {
const manifest = createPluginManifest();
const opaqueId = Symbol();
const plugin = new PluginWrapper({
Expand All @@ -219,16 +238,14 @@ test('`setup` fails if initializer does not return object', () => {
}),
});

mockPluginInitializer.mockReturnValue(null);
mockPluginInitializer.mockResolvedValue(null);

expect(() =>
plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {})
).toThrowErrorMatchingInlineSnapshot(
await expect(() => plugin.init()).rejects.toThrowErrorMatchingInlineSnapshot(
`"Initializer for plugin \\"some-plugin-id\\" is expected to return plugin instance, but returned \\"null\\"."`
);
});

test('`setup` fails if object returned from initializer does not define `setup` function', () => {
test('`init` fails if object returned from initializer does not define `setup` function', async () => {
const manifest = createPluginManifest();
const opaqueId = Symbol();
const plugin = new PluginWrapper({
Expand All @@ -245,11 +262,9 @@ test('`setup` fails if object returned from initializer does not define `setup`
});

const mockPluginInstance = { run: jest.fn() };
mockPluginInitializer.mockReturnValue(mockPluginInstance);
mockPluginInitializer.mockResolvedValue(mockPluginInstance);

expect(() =>
plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {})
).toThrowErrorMatchingInlineSnapshot(
await expect(() => plugin.init()).rejects.toThrowErrorMatchingInlineSnapshot(
`"Instance of plugin \\"some-plugin-id\\" does not define \\"setup\\" function."`
);
});
Expand All @@ -272,7 +287,9 @@ test('`setup` initializes plugin and calls appropriate lifecycle hook', async ()
});

const mockPluginInstance = { setup: jest.fn().mockResolvedValue({ contract: 'yes' }) };
mockPluginInitializer.mockReturnValue(mockPluginInstance);
mockPluginInitializer.mockResolvedValue(mockPluginInstance);

await plugin.init();

const setupContext = createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver });
const setupDependencies = { 'some-required-dep': { contract: 'no' } };
Expand Down Expand Up @@ -323,8 +340,9 @@ test('`start` fails invoked for the `preboot` plugin', async () => {
});

const mockPluginInstance = { setup: jest.fn() };
mockPluginInitializer.mockReturnValue(mockPluginInstance);
mockPluginInitializer.mockResolvedValue(mockPluginInstance);

await plugin.init();
await plugin.setup({} as any, {} as any);

expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot(
Expand Down Expand Up @@ -355,8 +373,9 @@ test('`start` calls plugin.start with context and dependencies', async () => {
setup: jest.fn(),
start: jest.fn().mockResolvedValue(pluginStartContract),
};
mockPluginInitializer.mockReturnValue(mockPluginInstance);
mockPluginInitializer.mockResolvedValue(mockPluginInstance);

await plugin.init();
await plugin.setup({} as any, {} as any);

const startContract = await plugin.start(context, deps);
Expand Down Expand Up @@ -399,8 +418,9 @@ test("`start` resolves `startDependencies` Promise after plugin's start", async
return pluginStartContract;
},
};
mockPluginInitializer.mockReturnValue(mockPluginInstance);
mockPluginInitializer.mockResolvedValue(mockPluginInstance);

await plugin.init();
await plugin.setup({} as any, {} as any);

const startDependenciesCheck = plugin.startDependencies.then((resolvedStartDeps) => {
Expand Down Expand Up @@ -429,7 +449,7 @@ test('`stop` fails if plugin is not set up', async () => {
});

const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() };
mockPluginInitializer.mockReturnValue(mockPluginInstance);
mockPluginInitializer.mockResolvedValue(mockPluginInstance);

await expect(plugin.stop()).rejects.toMatchInlineSnapshot(
`[Error: Plugin "some-plugin-id" can't be stopped since it isn't set up.]`
Expand All @@ -453,7 +473,8 @@ test('`stop` does nothing if plugin does not define `stop` function', async () =
}),
});

mockPluginInitializer.mockReturnValue({ setup: jest.fn() });
mockPluginInitializer.mockResolvedValue({ setup: jest.fn() });
await plugin.init();
await plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {});

await expect(plugin.stop()).resolves.toBeUndefined();
Expand All @@ -476,7 +497,8 @@ test('`stop` calls `stop` defined by the plugin instance', async () => {
});

const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() };
mockPluginInitializer.mockReturnValue(mockPluginInstance);
mockPluginInitializer.mockResolvedValue(mockPluginInstance);
await plugin.init();
await plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {});

await expect(plugin.stop()).resolves.toBeUndefined();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export class PluginWrapper<
this.includesUiPlugin = params.manifest.ui;
}

public async init() {
this.instance = await this.createPluginInstance();
}

/**
* Instantiates plugin and calls `setup` function exposed by the plugin initializer.
* @param setupContext Context that consists of various core services tailored specifically
Expand All @@ -98,7 +102,9 @@ export class PluginWrapper<
setupContext: CoreSetup<TPluginsStart> | CorePreboot,
plugins: TPluginsSetup
): TSetup | Promise<TSetup> {
this.instance = this.createPluginInstance();
if (!this.instance) {
throw new Error('The plugin is not initialized. Call the init method first.');
}

if (this.isPrebootPluginInstance(this.instance)) {
return this.instance.setup(setupContext as CorePreboot, plugins);
Expand Down Expand Up @@ -170,7 +176,7 @@ export class PluginWrapper<
return configDescriptor;
}

private createPluginInstance() {
private async createPluginInstance() {
this.log.debug('Initializing plugin');

// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand All @@ -186,7 +192,7 @@ export class PluginWrapper<
throw new Error(`Definition of plugin "${this.name}" should be a function (${this.path}).`);
}

const instance = initializer(this.initializerContext);
const instance = await initializer(this.initializerContext);
if (!instance || typeof instance !== 'object') {
throw new Error(
`Initializer for plugin "${
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function createPlugin(
type?: PluginType;
} = {}
): PluginWrapper<any, any> {
return new PluginWrapper<any, any>({
const plugin = new PluginWrapper<any, any>({
path: 'some-path',
manifest: {
id,
Expand All @@ -62,6 +62,8 @@ function createPlugin(
opaqueId: Symbol(id),
initializerContext: { logger } as any,
});
jest.spyOn(plugin, 'init').mockResolvedValue();
return plugin;
}

const prebootDeps = coreInternalLifecycleMock.createInternalPreboot();
Expand Down Expand Up @@ -602,7 +604,10 @@ describe('setup', () => {
mockCreatePluginSetupContext.mockImplementation(() => ({}));

const promise = pluginsSystem.setupPlugins(setupDeps);
jest.runAllTimers();
process.nextTick(() => {
// let the await init go through. then simulate the timeout
jest.runAllTimers();
});

await expect(promise).rejects.toMatchInlineSnapshot(
`[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]`
Expand Down
Loading