frontend-app-api: rename AppGraph to AppTree

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-10-28 11:31:28 +02:00
parent b4f8d0b59d
commit 4d6fa921db
16 changed files with 92 additions and 89 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': patch
---
Internal refactor to rename the app graph to app tree
@@ -28,7 +28,7 @@ import {
createRouteRef,
} from '@backstage/frontend-plugin-api';
import { MockConfigApi } from '@backstage/test-utils';
import { createAppGraph } from '../graph';
import { createAppTree } from '../tree';
import { Core } from '../extensions/Core';
import { CoreRoutes } from '../extensions/CoreRoutes';
import { CoreNav } from '../extensions/CoreNav';
@@ -77,13 +77,13 @@ function routeInfoFromExtensions(extensions: Extension<unknown>[]) {
id: 'test',
extensions,
});
const graph = createAppGraph({
const tree = createAppTree({
config: new MockConfigApi({}),
builtinExtensions: [Core, CoreRoutes, CoreNav, CoreLayout],
features: [plugin],
});
return extractRouteInfoFromAppNode(graph.root);
return extractRouteInfoFromAppNode(tree.root);
}
function sortedEntries<T>(map: Map<RouteRef, T>): [RouteRef, T][] {
@@ -18,7 +18,7 @@ import { RouteRef, coreExtensionData } from '@backstage/frontend-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { toLegacyPlugin } from '../wiring/createApp';
import { BackstageRouteObject } from './types';
import { AppNode } from '../graph';
import { AppNode } from '../tree';
// We always add a child that matches all subroutes but without any route refs. This makes
// sure that we're always able to match each route no matter how deep the navigation goes.
@@ -20,7 +20,7 @@ import {
createPlugin,
} from '@backstage/frontend-plugin-api';
import { MockConfigApi } from '@backstage/test-utils';
import { createAppGraph } from './createAppGraph';
import { createAppTree } from './createAppTree';
const extBase = {
id: 'test',
@@ -29,7 +29,7 @@ const extBase = {
factory: () => ({}),
};
describe('createAppGraph', () => {
describe('createAppTree', () => {
it('throws an error when a core extension is parametrized', () => {
const config = new MockConfigApi({
app: {
@@ -47,7 +47,7 @@ describe('createAppGraph', () => {
}),
];
expect(() =>
createAppGraph({ features, config, builtinExtensions: [] }),
createAppTree({ features, config, builtinExtensions: [] }),
).toThrow("Configuration of the 'core' extension is forbidden");
});
@@ -68,7 +68,7 @@ describe('createAppGraph', () => {
}),
];
expect(() =>
createAppGraph({ features, config, builtinExtensions: [] }),
createAppTree({ features, config, builtinExtensions: [] }),
).toThrow(
"It is forbidden to override the following extension(s): 'core', which is done by the following plugin(s): 'plugin'",
);
@@ -94,7 +94,7 @@ describe('createAppGraph', () => {
const features = [PluginA, PluginB];
expect(() =>
createAppGraph({ features, config, builtinExtensions: [] }),
createAppTree({ features, config, builtinExtensions: [] }),
).toThrow(
"The following extensions are duplicated: The extension 'A' was provided 2 time(s) by the plugin 'A' and 1 time(s) by the plugin 'B', The extension 'B' was provided 2 time(s) by the plugin 'B'",
);
@@ -102,7 +102,7 @@ describe('createAppGraph', () => {
it('throws an error when duplicated extension overrides are detected', () => {
expect(() =>
createAppGraph({
createAppTree({
features: [
createExtensionOverrides({
extensions: [
@@ -20,22 +20,22 @@ import {
ExtensionOverrides,
} from '@backstage/frontend-plugin-api';
import { readAppExtensionsConfig } from './readAppExtensionsConfig';
import { resolveAppGraph } from './resolveAppGraph';
import { resolveAppTree } from './resolveAppTree';
import { resolveAppNodeSpecs } from './resolveAppNodeSpecs';
import { AppGraph } from './types';
import { AppTree } from './types';
import { Config } from '@backstage/config';
import { instantiateAppNodeTree } from './instantiateAppNodeTree';
/** @internal */
export interface CreateAppGraphOptions {
export interface CreateAppTreeOptions {
features: (BackstagePlugin | ExtensionOverrides)[];
builtinExtensions: Extension<unknown>[];
config: Config;
}
/** @internal */
export function createAppGraph(options: CreateAppGraphOptions): AppGraph {
const appGraph = resolveAppGraph(
export function createAppTree(options: CreateAppTreeOptions): AppTree {
const tree = resolveAppTree(
'core',
resolveAppNodeSpecs({
features: options.features,
@@ -44,6 +44,6 @@ export function createAppGraph(options: CreateAppGraphOptions): AppGraph {
forbidden: new Set(['core']),
}),
);
instantiateAppNodeTree(appGraph.root);
return appGraph;
instantiateAppNodeTree(tree.root);
return tree;
}
@@ -20,4 +20,4 @@ export type {
AppNodeInstance,
AppNodeSpec,
} from './types';
export { createAppGraph } from './createAppGraph';
export { createAppTree } from './createAppTree';
@@ -26,7 +26,7 @@ import {
instantiateAppNodeTree,
} from './instantiateAppNodeTree';
import { AppNodeInstance, AppNodeSpec } from './types';
import { resolveAppGraph } from './resolveAppGraph';
import { resolveAppTree } from './resolveAppTree';
const testDataRef = createExtensionDataRef<string>('test');
const otherDataRef = createExtensionDataRef<number>('other');
@@ -79,30 +79,30 @@ function makeInstanceWithId<TConfig>(
describe('instantiateAppNodeTree', () => {
it('should instantiate a single node', () => {
const graph = resolveAppGraph('root-node', [
const tree = resolveAppTree('root-node', [
{ ...makeSpec(simpleExtension), id: 'root-node' },
]);
expect(graph.root.instance).not.toBeDefined();
instantiateAppNodeTree(graph.root);
expect(graph.root.instance).toBeDefined();
expect(graph.root.instance?.getData(testDataRef)).toBe('test');
expect(tree.root.instance).not.toBeDefined();
instantiateAppNodeTree(tree.root);
expect(tree.root.instance).toBeDefined();
expect(tree.root.instance?.getData(testDataRef)).toBe('test');
// Multiple calls should have no effect
instantiateAppNodeTree(graph.root);
expect(graph.root.instance).toBeDefined();
instantiateAppNodeTree(tree.root);
expect(tree.root.instance).toBeDefined();
});
it('should not instantiate disabled nodes', () => {
const graph = resolveAppGraph('root-node', [
const tree = resolveAppTree('root-node', [
{ ...makeSpec(simpleExtension), id: 'root-node', disabled: true },
]);
expect(graph.root.instance).not.toBeDefined();
instantiateAppNodeTree(graph.root);
expect(graph.root.instance).not.toBeDefined();
expect(tree.root.instance).not.toBeDefined();
instantiateAppNodeTree(tree.root);
expect(tree.root.instance).not.toBeDefined();
});
it('should instantiate a node with attachments', () => {
const graph = resolveAppGraph('root-node', [
const tree = resolveAppTree('root-node', [
{
...makeSpec(
createExtension({
@@ -127,26 +127,26 @@ describe('instantiateAppNodeTree', () => {
},
]);
const childNode = graph.nodes.get('child-node');
const childNode = tree.nodes.get('child-node');
expect(childNode).toBeDefined();
expect(graph.root.instance).not.toBeDefined();
expect(tree.root.instance).not.toBeDefined();
expect(childNode?.instance).not.toBeDefined();
instantiateAppNodeTree(graph.root);
expect(graph.root.instance).toBeDefined();
instantiateAppNodeTree(tree.root);
expect(tree.root.instance).toBeDefined();
expect(childNode?.instance).toBeDefined();
expect(graph.root.instance?.getData(inputMirrorDataRef)).toEqual({
expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({
test: [{ test: 'test' }],
});
// Multiple calls should have no effect
instantiateAppNodeTree(graph.root);
expect(graph.root.instance).toBeDefined();
instantiateAppNodeTree(tree.root);
expect(tree.root.instance).toBeDefined();
expect(childNode?.instance).toBeDefined();
});
it('should not instantiate disabled attachments', () => {
const graph = resolveAppGraph('root-node', [
const tree = resolveAppTree('root-node', [
{
...makeSpec(
createExtension({
@@ -172,15 +172,15 @@ describe('instantiateAppNodeTree', () => {
},
]);
const childNode = graph.nodes.get('child-node');
const childNode = tree.nodes.get('child-node');
expect(childNode).toBeDefined();
expect(graph.root.instance).not.toBeDefined();
expect(tree.root.instance).not.toBeDefined();
expect(childNode?.instance).not.toBeDefined();
instantiateAppNodeTree(graph.root);
expect(graph.root.instance).toBeDefined();
instantiateAppNodeTree(tree.root);
expect(tree.root.instance).toBeDefined();
expect(childNode?.instance).not.toBeDefined();
expect(graph.root.instance?.getData(inputMirrorDataRef)).toEqual({
expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({
test: [],
});
});
@@ -151,7 +151,7 @@ export function createAppNodeInstance(options: {
}
/**
* Starting at the provided node, instantiate all reachable nodes in the graph that have not been disabled.
* Starting at the provided node, instantiate all reachable nodes in the tree that have not been disabled.
* @internal
*/
export function instantiateAppNodeTree(rootNode: AppNode): void {
@@ -15,7 +15,7 @@
*/
import { createExtension } from '@backstage/frontend-plugin-api';
import { resolveAppGraph } from './resolveAppGraph';
import { resolveAppTree } from './resolveAppTree';
const extBaseConfig = {
id: 'test',
@@ -32,25 +32,25 @@ const baseSpec = {
disabled: false,
};
describe('buildAppGraph', () => {
it('should fail to create an empty graph', () => {
expect(() => resolveAppGraph('core', [])).toThrow(
"No root node with id 'core' found in app graph",
describe('buildAppTree', () => {
it('should fail to create an empty tree', () => {
expect(() => resolveAppTree('core', [])).toThrow(
"No root node with id 'core' found in app tree",
);
});
it('should create a graph with only one node', () => {
const graph = resolveAppGraph('core', [{ ...baseSpec, id: 'core' }]);
expect(graph.root).toEqual({
it('should create a tree with only one node', () => {
const tree = resolveAppTree('core', [{ ...baseSpec, id: 'core' }]);
expect(tree.root).toEqual({
spec: { ...baseSpec, id: 'core' },
edges: { attachments: new Map() },
});
expect(Array.from(graph.orphans)).toEqual([]);
expect(Array.from(graph.nodes.keys())).toEqual(['core']);
expect(Array.from(tree.orphans)).toEqual([]);
expect(Array.from(tree.nodes.keys())).toEqual(['core']);
});
it('should create a graph', () => {
const graph = resolveAppGraph('b', [
it('should create a tree', () => {
const tree = resolveAppTree('b', [
{ ...baseSpec, id: 'a' },
{ ...baseSpec, id: 'b' },
{ ...baseSpec, id: 'c' },
@@ -60,7 +60,7 @@ describe('buildAppGraph', () => {
{ ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' },
]);
expect(Array.from(graph.nodes.keys())).toEqual([
expect(Array.from(tree.nodes.keys())).toEqual([
'a',
'b',
'c',
@@ -70,7 +70,7 @@ describe('buildAppGraph', () => {
'dx1',
]);
expect(JSON.parse(JSON.stringify(graph.root))).toMatchInlineSnapshot(`
expect(JSON.parse(JSON.stringify(tree.root))).toMatchInlineSnapshot(`
{
"attachments": {
"x": [
@@ -90,7 +90,7 @@ describe('buildAppGraph', () => {
"id": "b",
}
`);
expect(String(graph.root)).toMatchInlineSnapshot(`
expect(String(tree.root)).toMatchInlineSnapshot(`
"<b>
x [
<bx1 />
@@ -102,7 +102,7 @@ describe('buildAppGraph', () => {
</b>"
`);
const orphans = Array.from(graph.orphans).map(String);
const orphans = Array.from(tree.orphans).map(String);
expect(orphans).toMatchInlineSnapshot(`
[
"<a />",
@@ -112,8 +112,8 @@ describe('buildAppGraph', () => {
`);
});
it('should create a graph out of order', () => {
const graph = resolveAppGraph('b', [
it('should create a tree out of order', () => {
const tree = resolveAppTree('b', [
{ ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' },
{ ...baseSpec, id: 'a' },
{ ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' },
@@ -123,7 +123,7 @@ describe('buildAppGraph', () => {
{ ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' },
]);
expect(Array.from(graph.nodes.keys())).toEqual([
expect(Array.from(tree.nodes.keys())).toEqual([
'bx2',
'a',
'by1',
@@ -133,7 +133,7 @@ describe('buildAppGraph', () => {
'dx1',
]);
expect(String(graph.root)).toMatchInlineSnapshot(`
expect(String(tree.root)).toMatchInlineSnapshot(`
"<b>
x [
<bx2 />
@@ -145,7 +145,7 @@ describe('buildAppGraph', () => {
</b>"
`);
const orphans = Array.from(graph.orphans).map(String);
const orphans = Array.from(tree.orphans).map(String);
expect(orphans).toMatchInlineSnapshot(`
[
"<a />",
@@ -157,7 +157,7 @@ describe('buildAppGraph', () => {
it('throws an error when duplicated extensions are detected', () => {
expect(() =>
resolveAppGraph('core', [
resolveAppTree('core', [
{ ...baseSpec, id: 'a' },
{ ...baseSpec, id: 'a' },
]),
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { AppGraph, AppNode, AppNodeInstance, AppNodeSpec } from './types';
import { AppTree, AppNode, AppNodeInstance, AppNodeSpec } from './types';
function indent(str: string) {
return str.replace(/^/gm, ' ');
@@ -83,17 +83,17 @@ class SerializableAppNode implements AppNode {
}
/**
* Build the app graph by iterating through all node specs and constructing the app
* Build the app tree by iterating through all node specs and constructing the app
* tree with all attachments in the same order as they appear in the input specs array.
* @internal
*/
export function resolveAppGraph(
export function resolveAppTree(
rootNodeId: string,
specs: AppNodeSpec[],
): AppGraph {
): AppTree {
const nodes = new Map<string, SerializableAppNode>();
// A node with the provided rootNodeId must be found in the graph, and it must not be attached to anything
// A node with the provided rootNodeId must be found in the tree, and it must not be attached to anything
let rootNode: AppNode | undefined = undefined;
// While iterating through the inputs specs we keep track of all nodes that were created
@@ -141,7 +141,7 @@ export function resolveAppGraph(
}
if (!rootNode) {
throw new Error(`No root node with id '${rootNodeId}' found in app graph`);
throw new Error(`No root node with id '${rootNodeId}' found in app tree`);
}
return {
@@ -25,13 +25,13 @@ NOTE: These types are marked as @internal for now, but the intention is for this
*/
/**
* The specification for this node in the app graph.
* The specification for this node in the app tree.
*
* @internal
* @remarks
*
* The specifications for a collection of app nodes is all the information needed
* to build the graph and instantiate the nodes.
* to build the tree and instantiate the nodes.
*/
export interface AppNodeSpec {
readonly id: string;
@@ -57,7 +57,7 @@ export interface AppNodeEdges {
}
/**
* The instance of this node in the app graph.
* The instance of this node in the app tree.
*
* @internal
* @remarks
@@ -80,18 +80,18 @@ export interface AppNodeInstance {
export interface AppNode {
/** The specification for how this node should be instantiated */
readonly spec: AppNodeSpec;
/** The edges from this node to other nodes in the app graph */
/** The edges from this node to other nodes in the app tree */
readonly edges: AppNodeEdges;
/** The instance of this node, if it was instantiated */
readonly instance?: AppNodeInstance;
}
/**
* The app graph containing all nodes of the app.
* The app tree containing all nodes of the app.
*
* @internal
*/
export interface AppGraph {
export interface AppTree {
/** The root node of the app */
root: AppNode;
/** A map of all nodes in the app by ID, including orphaned or disabled nodes */
@@ -87,7 +87,7 @@ import { AppRouteBinder } from '../routing';
import { RoutingProvider } from '../routing/RoutingProvider';
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
import { collectRouteIds } from '../routing/collectRouteIds';
import { AppNode, createAppGraph } from '../graph';
import { AppNode, createAppTree } from '../tree';
const builtinExtensions = [
Core,
@@ -117,7 +117,7 @@ export function createExtensionTree(options: {
config: Config;
}): ExtensionTree {
const features = getAvailableFeatures(options.config);
const graph = createAppGraph({
const tree = createAppTree({
features,
builtinExtensions,
config: options.config,
@@ -136,14 +136,14 @@ export function createExtensionTree(options: {
return {
getExtension(id: string): ExtensionTreeNode | undefined {
return convertNode(graph.nodes.get(id));
return convertNode(tree.nodes.get(id));
},
getExtensionAttachments(
id: string,
inputName: string,
): ExtensionTreeNode[] {
return (
graph.nodes
tree.nodes
.get(id)
?.edges.attachments.get(inputName)
?.map(convertNode)
@@ -247,7 +247,7 @@ export function createApp(options: {
...(options.features ?? []),
]);
const appGraph = createAppGraph({
const tree = createAppTree({
features: allFeatures,
builtinExtensions,
config,
@@ -262,11 +262,11 @@ export function createApp(options: {
const routeIds = collectRouteIds(allFeatures);
const App = () => (
<ApiProvider apis={createApiHolder(appGraph.root, config)}>
<ApiProvider apis={createApiHolder(tree.root, config)}>
<AppContextProvider appContext={appContext}>
<AppThemeProvider>
<RoutingProvider
{...extractRouteInfoFromAppNode(appGraph.root)}
{...extractRouteInfoFromAppNode(tree.root)}
routeBindings={resolveRouteBindings(
options.bindRoutes,
config,
@@ -275,9 +275,7 @@ export function createApp(options: {
>
{/* TODO: set base path using the logic from AppRouter */}
<BrowserRouter>
{appGraph.root.instance!.getData(
coreExtensionData.reactElement,
)}
{tree.root.instance!.getData(coreExtensionData.reactElement)}
</BrowserRouter>
</RoutingProvider>
</AppThemeProvider>