Skip to content

Commit 56f8f62

Browse files
authored
feat(plugin): add MultiPlugin type for registering multiple plugins from one module (#2641)
- Add MULTI_PLUGIN to PluginType enum - Define MultiPlugin interface with plugins: Plugin[] property - Add isMultiPlugin() type guard - Update loadModulePlugins() to flatten MultiPlugin inner plugins - Update PluginsBootstrap to handle MultiPlugin in core plugins - Rename PluginModule to PluginModuleExport (with deprecated alias) - Add tests for MultiPlugin type guard and loading - Tested using the dashboard resolve plugin branch: https://github.com/mofojed/deephaven-plugins/tree/DH-21757-homescreen-resolve - Ensured that the `WidgetPlugin` and the `DashboardPlugin` were both registered - Ensured widget data saved with the dashboard was opened like it used to, while opening deephaven.ui components open with the new mechanism
1 parent 6775c43 commit 56f8f62

6 files changed

Lines changed: 153 additions & 32 deletions

File tree

packages/app-utils/src/components/PluginsBootstrap.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
type Plugin,
44
type PluginModuleMap,
55
PluginsContext,
6+
isMultiPlugin,
7+
type MultiPlugin,
68
} from '@deephaven/plugin';
79
import { loadModulePlugins } from '../plugins';
810

@@ -12,8 +14,11 @@ export type PluginsBootstrapProps = {
1214
*/
1315
pluginsUrl: string;
1416

15-
/** The core plugins to load. */
16-
getCorePlugins?: () => Promise<Plugin[]>;
17+
/**
18+
* The core plugins to load.
19+
* Can include MultiPlugin instances which will be flattened.
20+
*/
21+
getCorePlugins?: () => Promise<(Plugin | MultiPlugin)[]>;
1722

1823
/**
1924
* The children to render wrapped with the PluginsContext.
@@ -38,9 +43,15 @@ export function PluginsBootstrap({
3843
const corePlugins = (await getCorePlugins?.()) ?? [];
3944
const pluginModules = await loadModulePlugins(pluginsUrl);
4045
if (!isCanceled) {
41-
const corePluginPairs = corePlugins.map(
42-
plugin => [plugin.name, plugin] as const
43-
);
46+
// Flatten MultiPlugins in core plugins
47+
const corePluginPairs = corePlugins.flatMap(plugin => {
48+
if (isMultiPlugin(plugin)) {
49+
return plugin.plugins.map(
50+
innerPlugin => [innerPlugin.name, innerPlugin] as const
51+
);
52+
}
53+
return [[plugin.name, plugin] as const];
54+
});
4455
setPlugins(new Map([...corePluginPairs, ...pluginModules]));
4556
}
4657
}

packages/app-utils/src/plugins/PluginUtils.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { type LegacyPlugin, type Plugin, PluginType } from '@deephaven/plugin';
1+
import {
2+
type LegacyPlugin,
3+
type MultiPlugin,
4+
type Plugin,
5+
PluginType,
6+
} from '@deephaven/plugin';
27
import { getPluginModuleValue } from './PluginUtils';
38

49
describe('getPluginModuleValue', () => {
@@ -126,4 +131,36 @@ describe('getPluginModuleValue', () => {
126131
const moduleValue = getPluginModuleValue({} as Plugin);
127132
expect(moduleValue).toBeNull();
128133
});
134+
135+
describe('MultiPlugin', () => {
136+
const multiPlugin: MultiPlugin = {
137+
name: 'test-multi-plugin',
138+
type: PluginType.MULTI_PLUGIN,
139+
plugins: [
140+
{ name: 'widget-plugin', type: PluginType.WIDGET_PLUGIN },
141+
{ name: 'dashboard-plugin', type: PluginType.DASHBOARD_PLUGIN },
142+
{ name: 'theme-plugin', type: PluginType.THEME_PLUGIN },
143+
] as Plugin[],
144+
};
145+
146+
it('supports MultiPlugin format', () => {
147+
const moduleValue = getPluginModuleValue(multiPlugin);
148+
expect(moduleValue).toBe(multiPlugin);
149+
});
150+
151+
it('supports MultiPlugin with default export', () => {
152+
const moduleWithDefault = { default: multiPlugin };
153+
const moduleValue = getPluginModuleValue(moduleWithDefault);
154+
expect(moduleValue).toBe(multiPlugin);
155+
});
156+
157+
it('supports MultiPlugin with named exports', () => {
158+
const moduleWithNamedExports = {
159+
default: multiPlugin,
160+
SomeNamedExport: 'value',
161+
};
162+
const moduleValue = getPluginModuleValue(moduleWithNamedExports);
163+
expect(moduleValue).toBe(multiPlugin);
164+
});
165+
});
129166
});

packages/app-utils/src/plugins/PluginUtils.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
isLegacyPlugin,
1313
type PluginModule,
1414
isPlugin,
15+
isMultiPlugin,
1516
} from '@deephaven/plugin';
1617
import loadRemoteModule from './loadRemoteModule';
1718

@@ -81,6 +82,27 @@ export function getPluginModuleValue(
8182
return null;
8283
}
8384

85+
/**
86+
* Register a plugin in the plugin map, logging a warning if a plugin with the same name already exists.
87+
* @param pluginMap The plugin map to register the plugin in
88+
* @param name The name to register the plugin under
89+
* @param plugin The plugin to register
90+
* @param version Optional version to attach to the plugin
91+
*/
92+
function registerPlugin(
93+
pluginMap: PluginModuleMap,
94+
name: string,
95+
plugin: PluginModule,
96+
version?: string
97+
): void {
98+
if (pluginMap.has(name)) {
99+
log.warn(
100+
`Plugin '${name}' is already registered. The existing plugin will be replaced.`
101+
);
102+
}
103+
pluginMap.set(name, { ...plugin, version });
104+
}
105+
84106
/**
85107
* Load all plugin modules available based on the manifest file at the provided base URL
86108
* @param modulePluginsUrl The base URL of the module plugins to load
@@ -115,8 +137,16 @@ export async function loadModulePlugins(
115137
const moduleValue = getPluginModuleValue(module.value);
116138
if (moduleValue == null) {
117139
log.error(`Plugin '${name}' is missing an exported value.`);
140+
} else if (isMultiPlugin(moduleValue)) {
141+
// Flatten MultiPlugin: register each inner plugin by its own name
142+
log.debug(
143+
`MultiPlugin '${name}' contains ${moduleValue.plugins.length} plugins`
144+
);
145+
moduleValue.plugins.forEach(innerPlugin => {
146+
registerPlugin(pluginMap, innerPlugin.name, innerPlugin, version);
147+
});
118148
} else {
119-
pluginMap.set(name, { ...moduleValue, version });
149+
registerPlugin(pluginMap, name, moduleValue, version);
120150
}
121151
} else {
122152
log.error(`Unable to load plugin '${name}'`, module.reason);

packages/plugin/src/PluginTypes.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {
22
PluginType,
33
isAuthPlugin,
44
isDashboardPlugin,
5+
isElementPlugin,
6+
isMultiPlugin,
57
isTablePlugin,
68
isThemePlugin,
79
isWidgetPlugin,
@@ -10,8 +12,10 @@ import {
1012
} from './PluginTypes';
1113

1214
const pluginTypeToTypeGuardMap = [
13-
[PluginType.DASHBOARD_PLUGIN, isDashboardPlugin],
1415
[PluginType.AUTH_PLUGIN, isAuthPlugin],
16+
[PluginType.DASHBOARD_PLUGIN, isDashboardPlugin],
17+
[PluginType.ELEMENT_PLUGIN, isElementPlugin],
18+
[PluginType.MULTI_PLUGIN, isMultiPlugin],
1519
[PluginType.TABLE_PLUGIN, isTablePlugin],
1620
[PluginType.THEME_PLUGIN, isThemePlugin],
1721
[PluginType.WIDGET_PLUGIN, isWidgetPlugin],

packages/plugin/src/PluginTypes.ts

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import type { TablePluginComponent } from './TablePlugin';
1010
export const PluginType = Object.freeze({
1111
AUTH_PLUGIN: 'AuthPlugin',
1212
DASHBOARD_PLUGIN: 'DashboardPlugin',
13-
WIDGET_PLUGIN: 'WidgetPlugin',
13+
ELEMENT_PLUGIN: 'ElementPlugin',
14+
MULTI_PLUGIN: 'MultiPlugin',
1415
TABLE_PLUGIN: 'TablePlugin',
1516
THEME_PLUGIN: 'ThemePlugin',
16-
ELEMENT_PLUGIN: 'ElementPlugin',
17+
WIDGET_PLUGIN: 'WidgetPlugin',
1718
});
1819

1920
/**
@@ -22,7 +23,7 @@ export const PluginType = Object.freeze({
2223
export type LegacyDashboardPlugin = { DashboardPlugin: React.ComponentType };
2324

2425
export function isLegacyDashboardPlugin(
25-
plugin: PluginModule
26+
plugin: PluginModuleExport
2627
): plugin is LegacyDashboardPlugin {
2728
return 'DashboardPlugin' in plugin;
2829
}
@@ -38,12 +39,12 @@ export type LegacyAuthPlugin = {
3839
};
3940

4041
export function isLegacyAuthPlugin(
41-
plugin: PluginModule
42+
plugin: PluginModuleExport
4243
): plugin is LegacyAuthPlugin {
4344
return 'AuthPlugin' in plugin;
4445
}
4546

46-
export type PluginModuleMap = Map<string, VersionedPluginModule>;
47+
export type PluginModuleMap = Map<string, VersionedPluginModuleExport>;
4748

4849
/**
4950
* @deprecated Use TablePlugin instead
@@ -53,7 +54,7 @@ export type LegacyTablePlugin = {
5354
};
5455

5556
export function isLegacyTablePlugin(
56-
plugin: PluginModule
57+
plugin: PluginModuleExport
5758
): plugin is LegacyTablePlugin {
5859
return 'TablePlugin' in plugin;
5960
}
@@ -68,15 +69,23 @@ export type LegacyPlugin =
6869

6970
export function isLegacyPlugin(plugin: unknown): plugin is LegacyPlugin {
7071
return (
71-
isLegacyDashboardPlugin(plugin as PluginModule) ||
72-
isLegacyAuthPlugin(plugin as PluginModule) ||
73-
isLegacyTablePlugin(plugin as PluginModule)
72+
isLegacyDashboardPlugin(plugin as PluginModuleExport) ||
73+
isLegacyAuthPlugin(plugin as PluginModuleExport) ||
74+
isLegacyTablePlugin(plugin as PluginModuleExport)
7475
);
7576
}
7677

77-
export type PluginModule = Plugin | LegacyPlugin;
78+
export type PluginModuleExport = Plugin | LegacyPlugin;
7879

79-
export type VersionedPluginModule = PluginModule & { version?: string };
80+
/** @deprecated Use PluginModuleExport instead */
81+
export type PluginModule = PluginModuleExport;
82+
83+
export type VersionedPluginModuleExport = PluginModuleExport & {
84+
version?: string;
85+
};
86+
87+
/** @deprecated Use VersionedPluginModuleExport instead */
88+
export type VersionedPluginModule = VersionedPluginModuleExport;
8089

8190
export interface Plugin {
8291
/**
@@ -104,7 +113,7 @@ export interface DashboardPlugin extends Plugin {
104113
}
105114

106115
export function isDashboardPlugin(
107-
plugin: PluginModule
116+
plugin: PluginModuleExport
108117
): plugin is DashboardPlugin {
109118
return 'type' in plugin && plugin.type === PluginType.DASHBOARD_PLUGIN;
110119
}
@@ -173,7 +182,9 @@ export interface WidgetPlugin<T = unknown> extends Plugin {
173182
icon?: IconDefinition | React.ReactElement<unknown>;
174183
}
175184

176-
export function isWidgetPlugin(plugin: PluginModule): plugin is WidgetPlugin {
185+
export function isWidgetPlugin(
186+
plugin: PluginModuleExport
187+
): plugin is WidgetPlugin {
177188
return 'type' in plugin && plugin.type === PluginType.WIDGET_PLUGIN;
178189
}
179190

@@ -182,7 +193,9 @@ export interface TablePlugin extends Plugin {
182193
component: TablePluginComponent;
183194
}
184195

185-
export function isTablePlugin(plugin: PluginModule): plugin is TablePlugin {
196+
export function isTablePlugin(
197+
plugin: PluginModuleExport
198+
): plugin is TablePlugin {
186199
return 'type' in plugin && plugin.type === PluginType.TABLE_PLUGIN;
187200
}
188201

@@ -219,7 +232,7 @@ export interface AuthPlugin extends Plugin {
219232
isAvailable: (authHandlers: string[], authConfig: AuthConfigMap) => boolean;
220233
}
221234

222-
export function isAuthPlugin(plugin: PluginModule): plugin is AuthPlugin {
235+
export function isAuthPlugin(plugin: PluginModuleExport): plugin is AuthPlugin {
223236
return 'type' in plugin && plugin.type === PluginType.AUTH_PLUGIN;
224237
}
225238

@@ -235,7 +248,9 @@ export interface ThemePlugin extends Plugin {
235248
}
236249

237250
/** Type guard to check if given plugin is a `ThemePlugin` */
238-
export function isThemePlugin(plugin: PluginModule): plugin is ThemePlugin {
251+
export function isThemePlugin(
252+
plugin: PluginModuleExport
253+
): plugin is ThemePlugin {
239254
return 'type' in plugin && plugin.type === PluginType.THEME_PLUGIN;
240255
}
241256

@@ -263,17 +278,40 @@ export interface ElementPlugin extends Plugin {
263278
mapping: ElementPluginMappingDefinition;
264279
}
265280

266-
export function isElementPlugin(plugin: PluginModule): plugin is ElementPlugin {
281+
export function isElementPlugin(
282+
plugin: PluginModuleExport
283+
): plugin is ElementPlugin {
267284
return 'type' in plugin && plugin.type === PluginType.ELEMENT_PLUGIN;
268285
}
269286

287+
/**
288+
* A plugin that contains multiple plugins.
289+
* When loaded, each plugin in the `plugins` array will be registered individually.
290+
*/
291+
export interface MultiPlugin extends Plugin {
292+
type: typeof PluginType.MULTI_PLUGIN;
293+
/**
294+
* The plugins to register. Each plugin will be registered by its own name.
295+
* Note: Nested MultiPlugins are not supported.
296+
*/
297+
plugins: Plugin[];
298+
}
299+
300+
/** Type guard to check if given plugin is a `MultiPlugin` */
301+
export function isMultiPlugin(
302+
plugin: PluginModuleExport
303+
): plugin is MultiPlugin {
304+
return 'type' in plugin && plugin.type === PluginType.MULTI_PLUGIN;
305+
}
306+
270307
export function isPlugin(plugin: unknown): plugin is Plugin {
271308
return (
272-
isDashboardPlugin(plugin as PluginModule) ||
273-
isAuthPlugin(plugin as PluginModule) ||
274-
isTablePlugin(plugin as PluginModule) ||
275-
isThemePlugin(plugin as PluginModule) ||
276-
isWidgetPlugin(plugin as PluginModule) ||
277-
isElementPlugin(plugin as PluginModule)
309+
isDashboardPlugin(plugin as PluginModuleExport) ||
310+
isAuthPlugin(plugin as PluginModuleExport) ||
311+
isElementPlugin(plugin as PluginModuleExport) ||
312+
isMultiPlugin(plugin as PluginModuleExport) ||
313+
isTablePlugin(plugin as PluginModuleExport) ||
314+
isThemePlugin(plugin as PluginModuleExport) ||
315+
isWidgetPlugin(plugin as PluginModuleExport)
278316
);
279317
}

packages/redux/src/store.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ export type PluginModule =
107107
| 'WidgetPlugin'
108108
| 'TablePlugin'
109109
| 'ThemePlugin'
110-
| 'ElementPlugin';
110+
| 'ElementPlugin'
111+
| 'MultiPlugin';
111112
}
112113
| {
113114
// eslint-disable-next-line @typescript-eslint/no-explicit-any

0 commit comments

Comments
 (0)