Skip to content

Commit c6ef5b3

Browse files
authored
feat: Plugin loader should prioritize new plugin format, when available (#1846)
We should be able to use both new and legacy plugin format in the same module, and plugin loader should prioritize the new format.
1 parent d91d833 commit c6ef5b3

File tree

4 files changed

+190
-8
lines changed

4 files changed

+190
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { LegacyPlugin, Plugin, PluginType } from '@deephaven/plugin';
2+
import { getPluginModuleValue } from './PluginUtils';
3+
4+
describe('getPluginModuleValue', () => {
5+
const legacyPlugins: [type: string, moduleValue: LegacyPlugin][] = [
6+
[
7+
'dashboard',
8+
{
9+
DashboardPlugin: () => null,
10+
},
11+
],
12+
[
13+
'auth',
14+
{
15+
AuthPlugin: {
16+
Component: () => null,
17+
isAvailable: () => true,
18+
},
19+
},
20+
],
21+
[
22+
'table',
23+
{
24+
TablePlugin: () => null,
25+
},
26+
],
27+
];
28+
29+
const newPlugins: [type: string, moduleValue: Plugin][] = Object.keys(
30+
PluginType
31+
).map(type => [type, { name: `${type}`, type: PluginType[type] }]);
32+
33+
const newPluginsWithNamedExports: [
34+
type: string,
35+
moduleValue: { default: Plugin; [key: string]: unknown },
36+
][] = Object.keys(PluginType).map(type => [
37+
type,
38+
{
39+
default: { name: `${type}Plugin`, type: PluginType[type] },
40+
NamedExport: 'NamedExportValue',
41+
},
42+
]);
43+
44+
const combinedPlugins: [
45+
type: string,
46+
moduleValue: {
47+
default: Plugin;
48+
} & LegacyPlugin,
49+
][] = [
50+
[
51+
'dashboard',
52+
{
53+
default: {
54+
name: 'combinedFormat1',
55+
type: PluginType.DASHBOARD_PLUGIN,
56+
},
57+
DashboardPlugin: () => null,
58+
},
59+
],
60+
[
61+
'auth',
62+
{
63+
default: {
64+
name: 'combinedFormat2',
65+
type: PluginType.AUTH_PLUGIN,
66+
},
67+
AuthPlugin: {
68+
Component: () => null,
69+
isAvailable: () => true,
70+
},
71+
},
72+
],
73+
[
74+
'table',
75+
{
76+
default: {
77+
name: 'combinedFormat3',
78+
type: PluginType.TABLE_PLUGIN,
79+
},
80+
TablePlugin: () => null,
81+
},
82+
],
83+
[
84+
// Should be able to combine different plugin types
85+
'multiple',
86+
{
87+
default: {
88+
name: 'widgetPlugin',
89+
type: PluginType.WIDGET_PLUGIN,
90+
},
91+
DashboardPlugin: () => null,
92+
},
93+
],
94+
];
95+
96+
it.each(legacyPlugins)(
97+
'supports legacy %s plugin format',
98+
(type, legacyPlugin) => {
99+
const moduleValue = getPluginModuleValue(legacyPlugin);
100+
expect(moduleValue).toBe(legacyPlugin);
101+
}
102+
);
103+
104+
it.each(newPlugins)('supports new %s format', (type, plugin) => {
105+
const moduleValue = getPluginModuleValue(plugin);
106+
expect(moduleValue).toBe(plugin);
107+
});
108+
109+
it.each(newPluginsWithNamedExports)(
110+
'supports new %s format with named exports',
111+
(type, plugin) => {
112+
const moduleValue = getPluginModuleValue(plugin);
113+
expect(moduleValue).toBe(plugin.default);
114+
}
115+
);
116+
117+
it.each(combinedPlugins)(
118+
'prioritizes new %s plugin if the module contains both legacy and new format',
119+
(type, plugin) => {
120+
const moduleValue = getPluginModuleValue(plugin);
121+
expect(moduleValue).toBe(plugin.default);
122+
}
123+
);
124+
125+
it('returns null if the module value is not a plugin', () => {
126+
const moduleValue = getPluginModuleValue({} as Plugin);
127+
expect(moduleValue).toBeNull();
128+
});
129+
});

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

+30-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
PluginType,
1111
isLegacyAuthPlugin,
1212
isLegacyPlugin,
13+
PluginModule,
14+
isPlugin,
1315
} from '@deephaven/plugin';
1416
import loadRemoteModule from './loadRemoteModule';
1517

@@ -52,6 +54,33 @@ export async function loadJson(jsonUrl: string): Promise<PluginManifest> {
5254
}
5355
}
5456

57+
function hasDefaultExport(value: unknown): value is { default: Plugin } {
58+
return (
59+
typeof value === 'object' &&
60+
value != null &&
61+
typeof (value as { default?: unknown }).default === 'object'
62+
);
63+
}
64+
65+
export function getPluginModuleValue(
66+
value: LegacyPlugin | Plugin | { default: Plugin }
67+
): PluginModule | null {
68+
// TypeScript builds CJS default exports differently depending on
69+
// whether there are also named exports. If the default is the only
70+
// export, it will be the value. If there are also named exports,
71+
// it will be assigned to the `default` property on the value.
72+
if (isPlugin(value)) {
73+
return value;
74+
}
75+
if (hasDefaultExport(value) && isPlugin(value.default)) {
76+
return value.default;
77+
}
78+
if (isLegacyPlugin(value)) {
79+
return value;
80+
}
81+
return null;
82+
}
83+
5584
/**
5685
* Load all plugin modules available based on the manifest file at the provided base URL
5786
* @param modulePluginsUrl The base URL of the module plugins to load
@@ -83,14 +112,7 @@ export async function loadModulePlugins(
83112
const module = pluginModules[i];
84113
const { name } = manifest.plugins[i];
85114
if (module.status === 'fulfilled') {
86-
const moduleValue = isLegacyPlugin(module.value)
87-
? module.value
88-
: // TypeScript builds CJS default exports differently depending on
89-
// whether there are also named exports. If the default is the only
90-
// export, it will be the value. If there are also named exports,
91-
// it will be assigned to the `default` property on the value.
92-
module.value.default ?? module.value;
93-
115+
const moduleValue = getPluginModuleValue(module.value);
94116
if (moduleValue == null) {
95117
log.error(`Plugin '${name}' is missing an exported value.`);
96118
} else {

packages/plugin/src/PluginTypes.test.ts

+21
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,25 @@ import {
44
isDashboardPlugin,
55
isTablePlugin,
66
isThemePlugin,
7+
isWidgetPlugin,
8+
Plugin,
9+
isPlugin,
710
} from './PluginTypes';
811

912
const pluginTypeToTypeGuardMap = [
1013
[PluginType.DASHBOARD_PLUGIN, isDashboardPlugin],
1114
[PluginType.AUTH_PLUGIN, isAuthPlugin],
1215
[PluginType.TABLE_PLUGIN, isTablePlugin],
1316
[PluginType.THEME_PLUGIN, isThemePlugin],
17+
[PluginType.WIDGET_PLUGIN, isWidgetPlugin],
1418
] as const;
1519

20+
const pluginTypeToPluginMap: [type: string, moduleValue: Plugin][] =
21+
Object.keys(PluginType).map(type => [
22+
type,
23+
{ name: `${type}`, type: PluginType[type] },
24+
]);
25+
1626
describe.each(pluginTypeToTypeGuardMap)(
1727
'plugin type guard: %s',
1828
(expectedPluginType, typeGuard) => {
@@ -26,3 +36,14 @@ describe.each(pluginTypeToTypeGuardMap)(
2636
);
2737
}
2838
);
39+
40+
describe('isPlugin', () => {
41+
it.each(pluginTypeToPluginMap)('returns true for %s type', (type, plugin) => {
42+
expect(isPlugin(plugin)).toBe(true);
43+
});
44+
45+
it('returns false for non-plugin types', () => {
46+
expect(isPlugin({ name: 'test', type: 'other' })).toBe(false);
47+
expect(isPlugin({})).toBe(false);
48+
});
49+
});

packages/plugin/src/PluginTypes.ts

+10
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,13 @@ export interface ThemePlugin extends Plugin {
226226
export function isThemePlugin(plugin: PluginModule): plugin is ThemePlugin {
227227
return 'type' in plugin && plugin.type === PluginType.THEME_PLUGIN;
228228
}
229+
230+
export function isPlugin(plugin: unknown): plugin is Plugin {
231+
return (
232+
isDashboardPlugin(plugin as PluginModule) ||
233+
isAuthPlugin(plugin as PluginModule) ||
234+
isTablePlugin(plugin as PluginModule) ||
235+
isThemePlugin(plugin as PluginModule) ||
236+
isWidgetPlugin(plugin as PluginModule)
237+
);
238+
}

0 commit comments

Comments
 (0)