frontend-plugin-api: promote PluginWrapper API to stable with useWrapperValue hook
Promote PluginWrapperApi, pluginWrapperApiRef, PluginWrapperBlueprint, and the new PluginWrapperDefinition type from @alpha to @public. Add getRootWrapper() method to PluginWrapperApi and integrate the useWrapperValue hook pattern from the rugvip/plugin-wrapper branch, allowing plugin wrappers to share stateful values via a hook that runs once in the root wrapper and is distributed to all wrapper instances via useSyncExternalStore. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -11,8 +11,11 @@ import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
// @alpha
|
||||
// @public
|
||||
export type PluginWrapperApi = {
|
||||
getRootWrapper(): ComponentType<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
getPluginWrapper(pluginId: string):
|
||||
| ComponentType<{
|
||||
children: ReactNode;
|
||||
@@ -20,31 +23,19 @@ export type PluginWrapperApi = {
|
||||
| undefined;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
// @public
|
||||
export const pluginWrapperApiRef: ApiRef<PluginWrapperApi>;
|
||||
|
||||
// @alpha
|
||||
// @public
|
||||
export const PluginWrapperBlueprint: ExtensionBlueprint<{
|
||||
kind: 'plugin-wrapper';
|
||||
params: (params: {
|
||||
loader: () => Promise<{
|
||||
component: ComponentType<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
}>;
|
||||
params: <TValue = never>(params: {
|
||||
loader: () => Promise<PluginWrapperDefinition<TValue>>;
|
||||
}) => ExtensionBlueprintParams<{
|
||||
loader: () => Promise<{
|
||||
component: ComponentType<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
}>;
|
||||
loader: () => Promise<PluginWrapperDefinition>;
|
||||
}>;
|
||||
output: ExtensionDataRef<
|
||||
() => Promise<{
|
||||
component: ComponentType<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
}>,
|
||||
() => Promise<PluginWrapperDefinition>,
|
||||
'core.plugin-wrapper.loader',
|
||||
{}
|
||||
>;
|
||||
@@ -53,16 +44,21 @@ export const PluginWrapperBlueprint: ExtensionBlueprint<{
|
||||
configInput: {};
|
||||
dataRefs: {
|
||||
wrapper: ConfigurableExtensionDataRef<
|
||||
() => Promise<{
|
||||
component: ComponentType<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
}>,
|
||||
() => Promise<PluginWrapperDefinition>,
|
||||
'core.plugin-wrapper.loader',
|
||||
{}
|
||||
>;
|
||||
};
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export type PluginWrapperDefinition<TValue = unknown | never> = {
|
||||
useWrapperValue?: () => TValue;
|
||||
component: ComponentType<{
|
||||
children: ReactNode;
|
||||
value: TValue;
|
||||
}>;
|
||||
};
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -1880,6 +1880,55 @@ export interface PluginOptions<
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type PluginWrapperApi = {
|
||||
getRootWrapper(): ComponentType<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
getPluginWrapper(pluginId: string):
|
||||
| ComponentType<{
|
||||
children: ReactNode;
|
||||
}>
|
||||
| undefined;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const pluginWrapperApiRef: ApiRef_2<PluginWrapperApi>;
|
||||
|
||||
// @public
|
||||
export const PluginWrapperBlueprint: ExtensionBlueprint_2<{
|
||||
kind: 'plugin-wrapper';
|
||||
params: <TValue = never>(params: {
|
||||
loader: () => Promise<PluginWrapperDefinition<TValue>>;
|
||||
}) => ExtensionBlueprintParams_2<{
|
||||
loader: () => Promise<PluginWrapperDefinition>;
|
||||
}>;
|
||||
output: ExtensionDataRef_2<
|
||||
() => Promise<PluginWrapperDefinition>,
|
||||
'core.plugin-wrapper.loader',
|
||||
{}
|
||||
>;
|
||||
inputs: {};
|
||||
config: {};
|
||||
configInput: {};
|
||||
dataRefs: {
|
||||
wrapper: ConfigurableExtensionDataRef_2<
|
||||
() => Promise<PluginWrapperDefinition>,
|
||||
'core.plugin-wrapper.loader',
|
||||
{}
|
||||
>;
|
||||
};
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export type PluginWrapperDefinition<TValue = unknown | never> = {
|
||||
useWrapperValue?: () => TValue;
|
||||
component: ComponentType<{
|
||||
children: ReactNode;
|
||||
value: TValue;
|
||||
}>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type PortableSchema<TOutput, TInput = TOutput> = {
|
||||
parse: (input: TInput) => TOutput;
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { PluginWrapperBlueprint } from './blueprints/PluginWrapperBlueprint';
|
||||
// These exports are now available from the main entry point and are
|
||||
// re-exported here only for backwards compatibility.
|
||||
export {
|
||||
PluginWrapperBlueprint,
|
||||
type PluginWrapperDefinition,
|
||||
} from './blueprints/PluginWrapperBlueprint';
|
||||
export {
|
||||
type PluginWrapperApi,
|
||||
pluginWrapperApiRef,
|
||||
|
||||
@@ -15,21 +15,23 @@
|
||||
*/
|
||||
|
||||
import { ComponentType, ReactNode } from 'react';
|
||||
import { createApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { createApiRef } from '../system';
|
||||
|
||||
/**
|
||||
* The Plugin Wrapper API is used to wrap plugin extensions with providers,
|
||||
* plugins should generally use `ExtensionBoundary` instead.
|
||||
* The Plugin Wrapper API allows plugins to wrap their extensions with
|
||||
* providers. This API is only intended for internal use by the Backstage
|
||||
* frontend system. To provide contexts to plugin components, use
|
||||
* `ExtensionBoundary` instead.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This API is primarily intended for internal use by the Backstage frontend
|
||||
* system, but can be used for advanced use-cases. If you do override it, be
|
||||
* sure to include the default implementation as well.
|
||||
*
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export type PluginWrapperApi = {
|
||||
/**
|
||||
* Returns the root wrapper that manages the global plugin state across
|
||||
* plugin wrapper instances.
|
||||
*/
|
||||
getRootWrapper(): ComponentType<{ children: ReactNode }>;
|
||||
|
||||
/**
|
||||
* Returns a wrapper component for a specific plugin, or undefined if no
|
||||
* wrappers exist. Do not use this API directly, instead use
|
||||
@@ -43,8 +45,8 @@ export type PluginWrapperApi = {
|
||||
/**
|
||||
* The API reference of {@link PluginWrapperApi}.
|
||||
*
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export const pluginWrapperApiRef = createApiRef<PluginWrapperApi>({
|
||||
id: 'core.plugin-wrapper.alpha',
|
||||
id: 'core.plugin-wrapper',
|
||||
});
|
||||
|
||||
@@ -50,3 +50,4 @@ export * from './StorageApi';
|
||||
export * from './AnalyticsApi';
|
||||
export * from './TranslationApi';
|
||||
export * from './PluginHeaderActionsApi';
|
||||
export * from './PluginWrapperApi';
|
||||
|
||||
@@ -21,14 +21,42 @@ import {
|
||||
createExtensionDataRef,
|
||||
} from '../wiring';
|
||||
|
||||
/**
|
||||
* Defines the structure of a plugin wrapper, optionally including a shared
|
||||
* hook value.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* When `useWrapperValue` is provided, the hook is called in a single location
|
||||
* in the app and the resulting value is forwarded as the `value` prop to the
|
||||
* component. The hook obeys the rules of React hooks and is not called until a
|
||||
* component from the plugin is rendered.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PluginWrapperDefinition<TValue = unknown | never> = {
|
||||
/**
|
||||
* Creates a shared value that is forwarded as the `value` prop to the
|
||||
* component.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This function obeys the rules of React hooks and is only invoked in a
|
||||
* single location in the app. Note that the hook will not be called until a
|
||||
* component from the plugin is rendered.
|
||||
*/
|
||||
useWrapperValue?: () => TValue;
|
||||
component: ComponentType<{ children: ReactNode; value: TValue }>;
|
||||
};
|
||||
|
||||
const wrapperDataRef = createExtensionDataRef<
|
||||
() => Promise<{ component: ComponentType<{ children: ReactNode }> }>
|
||||
() => Promise<PluginWrapperDefinition>
|
||||
>().with({ id: 'core.plugin-wrapper.loader' });
|
||||
|
||||
/**
|
||||
* Creates extensions that wrap plugin extensions with providers.
|
||||
*
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export const PluginWrapperBlueprint = createExtensionBlueprint({
|
||||
kind: 'plugin-wrapper',
|
||||
@@ -37,12 +65,12 @@ export const PluginWrapperBlueprint = createExtensionBlueprint({
|
||||
dataRefs: {
|
||||
wrapper: wrapperDataRef,
|
||||
},
|
||||
defineParams(params: {
|
||||
loader: () => Promise<{
|
||||
component: ComponentType<{ children: ReactNode }>;
|
||||
}>;
|
||||
defineParams<TValue = never>(params: {
|
||||
loader: () => Promise<PluginWrapperDefinition<TValue>>;
|
||||
}) {
|
||||
return createExtensionBlueprintParams(params);
|
||||
return createExtensionBlueprintParams(
|
||||
params as { loader: () => Promise<PluginWrapperDefinition> },
|
||||
);
|
||||
},
|
||||
*factory(params) {
|
||||
yield wrapperDataRef(params.loader);
|
||||
|
||||
@@ -24,3 +24,7 @@ export { NavItemBlueprint } from './NavItemBlueprint';
|
||||
export { PageBlueprint } from './PageBlueprint';
|
||||
export { SubPageBlueprint } from './SubPageBlueprint';
|
||||
export { PluginHeaderActionBlueprint } from './PluginHeaderActionBlueprint';
|
||||
export {
|
||||
PluginWrapperBlueprint,
|
||||
type PluginWrapperDefinition,
|
||||
} from './PluginWrapperBlueprint';
|
||||
|
||||
@@ -144,6 +144,10 @@ describe('ExtensionBoundary', () => {
|
||||
};
|
||||
|
||||
const pluginWrapperApi: PluginWrapperApi = {
|
||||
getRootWrapper:
|
||||
() =>
|
||||
({ children }: { children: ReactNode }) =>
|
||||
<>{children}</>,
|
||||
getPluginWrapper: jest.fn((pluginId: string) => {
|
||||
if (pluginId === 'app') {
|
||||
return WrapperComponent;
|
||||
@@ -180,6 +184,10 @@ describe('ExtensionBoundary', () => {
|
||||
};
|
||||
|
||||
const pluginWrapperApi: PluginWrapperApi = {
|
||||
getRootWrapper:
|
||||
() =>
|
||||
({ children }: { children: ReactNode }) =>
|
||||
<>{children}</>,
|
||||
getPluginWrapper: jest.fn((pluginId: string) => {
|
||||
if (pluginId === 'app') {
|
||||
return ThrowingWrapper;
|
||||
|
||||
Reference in New Issue
Block a user