Skip to content

Commit 6b5f505

Browse files
author
=
committed
refactor(extension-point): translate to ts
1 parent e8ec797 commit 6b5f505

File tree

7 files changed

+208
-143
lines changed

7 files changed

+208
-143
lines changed

lib/static/components/extension-point.jsx

-107
This file was deleted.
+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React, {Component, FC, ReactNode} from 'react';
2+
import * as plugins from '../modules/plugins';
3+
import {PLUGIN_COMPONENT_POSITIONS, PluginComponentPosition, PluginDescription} from '@/types';
4+
import ErrorBoundary from './error-boundary';
5+
6+
interface ExtensionPointProps {
7+
name: string;
8+
children?: React.ReactNode;
9+
}
10+
11+
interface ExtensionPointComponent {
12+
PluginComponent: FC;
13+
name: string;
14+
point: string;
15+
position: PluginComponentPosition;
16+
config: PluginDescription;
17+
}
18+
19+
type ExtensionPointComponentUnchecked =
20+
Omit<ExtensionPointComponent, 'point' | 'position'>
21+
& Partial<Pick<ExtensionPointComponent, 'point' | 'position'>>
22+
23+
export default class ExtensionPoint<T extends ExtensionPointProps> extends Component<T> {
24+
render(): ReactNode {
25+
const loadedPluginConfigs = plugins.getLoadedConfigs();
26+
27+
if (loadedPluginConfigs.length) {
28+
const {name: pointName, children: reportComponent, ...componentProps} = this.props;
29+
const pluginComponents = getExtensionPointComponents(loadedPluginConfigs, pointName);
30+
return getComponentsComposition(pluginComponents, reportComponent, componentProps);
31+
}
32+
33+
return this.props.children;
34+
}
35+
}
36+
37+
function getComponentsComposition(pluginComponents: ExtensionPointComponent[], reportComponent: ReactNode, componentProps: any): ReactNode {
38+
let currentComponent = reportComponent;
39+
40+
for (const {PluginComponent, position, config} of pluginComponents) {
41+
currentComponent = composeComponents(PluginComponent, componentProps, currentComponent, position, config);
42+
}
43+
44+
return currentComponent;
45+
}
46+
47+
function composeComponents(PluginComponent: FC, pluginProps: any, currentComponent: ReactNode, position: PluginComponentPosition, config: PluginDescription): ReactNode {
48+
switch (position) {
49+
case 'wrap':
50+
return <ErrorBoundary fallback={currentComponent}>
51+
<PluginComponent {...pluginProps}>
52+
{currentComponent}
53+
</PluginComponent>
54+
</ErrorBoundary>;
55+
case 'before':
56+
return <>
57+
<ErrorBoundary>
58+
<PluginComponent {...pluginProps}/>
59+
</ErrorBoundary>
60+
{currentComponent}
61+
</>;
62+
case 'after':
63+
return <>
64+
{currentComponent}
65+
<ErrorBoundary>
66+
<PluginComponent {...pluginProps}/>
67+
</ErrorBoundary>
68+
</>;
69+
default:
70+
console.error(`${getComponentSpec(config)} unexpected position "${position}" specified.`);
71+
return currentComponent;
72+
}
73+
}
74+
75+
function getExtensionPointComponents(loadedPluginConfigs: PluginDescription[], pointName: string): ExtensionPointComponent[] {
76+
return loadedPluginConfigs
77+
.map<ExtensionPointComponentUnchecked>(pluginDescription => {
78+
try {
79+
const PluginComponent = plugins.getPluginField<FC>(pluginDescription.name, pluginDescription.component);
80+
81+
return {
82+
PluginComponent,
83+
name: pluginDescription.name,
84+
point: getComponentPoint(PluginComponent, pluginDescription),
85+
position: getComponentPosition(PluginComponent, pluginDescription),
86+
config: pluginDescription
87+
};
88+
} catch (err) {
89+
console.error(err);
90+
return {} as ExtensionPointComponentUnchecked;
91+
}
92+
})
93+
.filter((component: ExtensionPointComponentUnchecked): component is ExtensionPointComponent => {
94+
return Boolean(component.point && component.position && component.point === pointName);
95+
});
96+
}
97+
98+
function getComponentPoint(component: FC, config: PluginDescription): string | undefined {
99+
const result = getComponentConfigField(component, config, 'point');
100+
101+
if (typeof result !== 'string') {
102+
return;
103+
}
104+
105+
return result as string;
106+
}
107+
108+
function getComponentPosition(component: FC, config: PluginDescription): PluginComponentPosition | undefined {
109+
const result = getComponentConfigField(component, config, 'position');
110+
111+
if (typeof result !== 'string') {
112+
return;
113+
}
114+
115+
if (!PLUGIN_COMPONENT_POSITIONS.includes(result as PluginComponentPosition)) {
116+
return;
117+
}
118+
119+
return result as PluginComponentPosition;
120+
}
121+
122+
function getComponentConfigField(component: any, config: any, field: string): unknown | null {
123+
if (component[field] && config[field] && component[field] !== config[field]) {
124+
console.error(`${getComponentSpec(config)} "${field}" field does not match the one from the config: "${component[field]}" vs "${config[field]}".`);
125+
return null;
126+
} else if (!component[field] && !config[field]) {
127+
console.error(`${getComponentSpec(config)} "${field}" field is not set.`);
128+
return null;
129+
}
130+
131+
return component[field] || config[field];
132+
}
133+
134+
function getComponentSpec(pluginDescription: PluginDescription): string {
135+
return `Component "${pluginDescription.component}" of "${pluginDescription.name}" plugin`;
136+
}

lib/static/modules/load-plugin.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, {Component, FC} from 'react';
22
import * as Redux from 'redux';
33
import * as ReactRedux from 'react-redux';
44
import _ from 'lodash';
@@ -13,6 +13,7 @@ import axios from 'axios';
1313
import * as selectors from './selectors';
1414
import actionNames from './action-names';
1515
import Details from '../components/details';
16+
import {PluginConfig} from '@/types';
1617

1718
const whitelistedDeps = {
1819
'react': React,
@@ -39,9 +40,11 @@ type WhitelistedDep = typeof whitelistedDeps[WhitelistedDepName];
3940
// Branded string
4041
type ScriptText = string & {__script_text__: never};
4142

42-
type UnknownRecord = {[key: string]: unknown}
43-
export type InstalledPlugin = UnknownRecord
44-
export type PluginConfig = UnknownRecord
43+
export type InstalledPlugin = {
44+
name?: string;
45+
component?: FC | Component;
46+
[key: string]: unknown;
47+
}
4548

4649
interface PluginOptions {
4750
pluginName: string;
@@ -81,7 +84,7 @@ export function preloadPlugin(pluginName: string): void {
8184
loadingPlugins[pluginName] = loadingPlugins[pluginName] || getScriptText(pluginName);
8285
}
8386

84-
export async function loadPlugin(pluginName: string, pluginConfig: PluginConfig): Promise<InstalledPlugin | undefined> {
87+
export async function loadPlugin(pluginName: string, pluginConfig?: PluginConfig): Promise<InstalledPlugin | undefined> {
8588
if (pendingPlugins[pluginName]) {
8689
return pendingPlugins[pluginName];
8790
}
@@ -103,7 +106,7 @@ const hasDefault = (plugin: CompiledPlugin): plugin is ModuleWithDefaultFunction
103106
const getDeps = (pluginWithDeps: CompiledPluginWithDeps): WhitelistedDepName[] => pluginWithDeps.slice(0, -1) as WhitelistedDepName[];
104107
const getPluginFn = (pluginWithDeps: CompiledPluginWithDeps): PluginFunction => _.last(pluginWithDeps) as PluginFunction;
105108

106-
async function initPlugin(plugin: CompiledPlugin, pluginName: string, pluginConfig: PluginConfig): Promise<InstalledPlugin | undefined> {
109+
async function initPlugin(plugin: CompiledPlugin, pluginName: string, pluginConfig?: PluginConfig): Promise<InstalledPlugin | undefined> {
107110
try {
108111
if (!_.isObject(plugin)) {
109112
return undefined;

lib/static/modules/plugins.ts

+17-20
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,35 @@
1-
import {InstalledPlugin, loadPlugin, PluginConfig, preloadPlugin} from './load-plugin';
1+
import {ConfigForStaticFile} from '@/server-utils';
2+
import {InstalledPlugin, loadPlugin, preloadPlugin} from './load-plugin';
3+
import {PluginDescription} from '@/types';
24

3-
interface PluginSetupInfo {
4-
name: string
5-
config: PluginConfig
6-
}
7-
8-
interface Config {
9-
pluginsEnabled: boolean;
10-
plugins: PluginSetupInfo[]
11-
}
5+
export type ExtensionPointComponentPosition = 'wrap' | 'before' | 'after'
126

137
const plugins: Record<string, InstalledPlugin> = {};
14-
const loadedPluginConfigs: PluginSetupInfo[] = [];
8+
const loadedPluginConfigs: PluginDescription[] = [];
159

16-
export function preloadAll(config: Config): void {
10+
export function preloadAll(config?: ConfigForStaticFile): void {
1711
if (!config || !config.pluginsEnabled || !Array.isArray(config.plugins)) {
1812
return;
1913
}
2014

2115
config.plugins.forEach(plugin => preloadPlugin(plugin.name));
2216
}
2317

24-
export async function loadAll(config: Config): Promise<void> {
18+
export async function loadAll(config?: ConfigForStaticFile): Promise<void> {
2519
// if plugins are disabled, act like there are no plugins defined
2620
if (!config || !config.pluginsEnabled || !Array.isArray(config.plugins)) {
2721
return;
2822
}
2923

30-
const pluginsSetupInfo = await Promise.all(config.plugins.map(async pluginConfig => {
31-
const plugin = await loadPlugin(pluginConfig.name, pluginConfig.config);
24+
const pluginsSetupInfo = await Promise.all(config.plugins.map(async pluginDescription => {
25+
const plugin = await loadPlugin(pluginDescription.name, pluginDescription.config);
26+
3227
if (plugin) {
33-
plugins[pluginConfig.name] = plugin;
34-
return pluginConfig;
28+
plugins[pluginDescription.name] = plugin;
29+
return pluginDescription;
3530
}
31+
32+
return undefined;
3633
}));
3734

3835
pluginsSetupInfo.map((setupInfo) => {
@@ -60,17 +57,17 @@ export function forEachPlugin(callback: ForEachPluginCallback): void {
6057
});
6158
}
6259

63-
export function getPluginField(name: string, field: string): unknown {
60+
export function getPluginField<T>(name: string, field: string): T {
6461
const plugin = plugins[name];
6562
if (!plugin) {
6663
throw new Error(`Plugin "${name}" is not loaded.`);
6764
}
6865
if (!plugin[field]) {
6966
throw new Error(`"${field}" is not defined on plugin "${name}".`);
7067
}
71-
return plugins[name][field];
68+
return plugins[name][field] as T;
7269
}
7370

74-
export function getLoadedConfigs(): PluginSetupInfo[] {
71+
export function getLoadedConfigs(): PluginDescription[] {
7572
return loadedPluginConfigs;
7673
}

0 commit comments

Comments
 (0)