core-api: refactor all element data collection to use the same traversal facilities
Co-authored-by: blam <ben@blam.sh>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { Children, isValidElement } from 'react';
|
||||
import {
|
||||
childDiscoverer,
|
||||
createCollector,
|
||||
traverseElementTree,
|
||||
} from './traversal';
|
||||
|
||||
describe('discovery', () => {
|
||||
it('should collect element names', () => {
|
||||
const root = (
|
||||
<main>
|
||||
<div>
|
||||
<h1>Title</h1>
|
||||
<p>Text</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<h2>Title</h2>
|
||||
<span>Text</span>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
const { names } = traverseElementTree({
|
||||
root,
|
||||
discoverers: [childDiscoverer],
|
||||
collectors: {
|
||||
names: createCollector(Array<string>(), (acc, el) => {
|
||||
if (typeof el.type === 'string') {
|
||||
acc.push(el.type);
|
||||
}
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
expect(names).toEqual([
|
||||
'main',
|
||||
'div',
|
||||
'hr',
|
||||
'div',
|
||||
'h1',
|
||||
'p',
|
||||
'h2',
|
||||
'span',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should collect element names while skipping one level of children', () => {
|
||||
const root = (
|
||||
<main>
|
||||
<div>
|
||||
<h1>Title</h1>
|
||||
<p>Text</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<h2>Title</h2>
|
||||
<span>Text</span>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
const { names } = traverseElementTree({
|
||||
root,
|
||||
discoverers: [
|
||||
el =>
|
||||
Children.toArray(el.props.children).flatMap(child =>
|
||||
isValidElement(child) ? child?.props?.children : [],
|
||||
),
|
||||
],
|
||||
collectors: {
|
||||
names: createCollector(Array<string>(), (acc, el) => {
|
||||
if (typeof el.type === 'string') {
|
||||
acc.push(el.type);
|
||||
}
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
expect(names).toEqual(['main', 'h1', 'p', 'h2', 'span']);
|
||||
});
|
||||
});
|
||||
+9
-73
@@ -15,12 +15,10 @@
|
||||
*/
|
||||
|
||||
import { isValidElement, ReactNode, ReactElement, Children } from 'react';
|
||||
import { RouteRef } from './types';
|
||||
import { getComponentData } from '../extensions';
|
||||
|
||||
type Discoverer = (element: ReactElement) => ReactNode;
|
||||
export type Discoverer = (element: ReactElement) => ReactNode;
|
||||
|
||||
type Collector<Result, Context> = () => {
|
||||
export type Collector<Result, Context> = () => {
|
||||
accumulator: Result;
|
||||
visit(
|
||||
accumulator: Result,
|
||||
@@ -121,6 +119,13 @@ export function traverseElementTree<Results>(options: {
|
||||
) as Results;
|
||||
}
|
||||
|
||||
export function createCollector<Result, Context>(
|
||||
initialResult: Result,
|
||||
visit: ReturnType<Collector<Result, Context>>['visit'],
|
||||
): Collector<Result, Context> {
|
||||
return () => ({ accumulator: initialResult, visit });
|
||||
}
|
||||
|
||||
export function childDiscoverer(element: ReactElement): ReactNode {
|
||||
return element.props?.children;
|
||||
}
|
||||
@@ -131,72 +136,3 @@ export function routeElementDiscoverer(element: ReactElement): ReactNode {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createCollector<Result, Context>(
|
||||
initialResult: Result,
|
||||
visit: ReturnType<Collector<Result, Context>>['visit'],
|
||||
): Collector<Result, Context> {
|
||||
return () => ({ accumulator: initialResult, visit });
|
||||
}
|
||||
|
||||
export const routeCollector = createCollector(
|
||||
new Map<RouteRef, string>(),
|
||||
(acc, node, parent) => {
|
||||
if (parent.props.element === node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path: string | undefined = node.props?.path;
|
||||
const element: ReactNode = node.props?.element;
|
||||
|
||||
const routeRef = getComponentData<RouteRef>(node, 'core.mountPoint');
|
||||
if (routeRef) {
|
||||
if (!path) {
|
||||
throw new Error('Mounted routable extension must have a path');
|
||||
}
|
||||
acc.set(routeRef, path);
|
||||
} else if (isValidElement(element)) {
|
||||
const elementRouteRef = getComponentData<RouteRef>(
|
||||
element,
|
||||
'core.mountPoint',
|
||||
);
|
||||
if (elementRouteRef) {
|
||||
if (!path) {
|
||||
throw new Error('Route element must have a path');
|
||||
}
|
||||
acc.set(elementRouteRef, path);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const routeParentCollector = createCollector(
|
||||
new Map<RouteRef, RouteRef | undefined>(),
|
||||
(acc, node, parent, parentRouteRef?: RouteRef) => {
|
||||
if (parent.props.element === node) {
|
||||
return parentRouteRef;
|
||||
}
|
||||
|
||||
const element: ReactNode = node.props?.element;
|
||||
|
||||
let nextParent = parentRouteRef;
|
||||
|
||||
const routeRef = getComponentData<RouteRef>(node, 'core.mountPoint');
|
||||
if (routeRef) {
|
||||
acc.set(routeRef, parentRouteRef);
|
||||
nextParent = routeRef;
|
||||
} else if (isValidElement(element)) {
|
||||
const elementRouteRef = getComponentData<RouteRef>(
|
||||
element,
|
||||
'core.mountPoint',
|
||||
);
|
||||
|
||||
if (elementRouteRef) {
|
||||
acc.set(elementRouteRef, parentRouteRef);
|
||||
nextParent = elementRouteRef;
|
||||
}
|
||||
}
|
||||
|
||||
return nextParent;
|
||||
},
|
||||
);
|
||||
+16
-3
@@ -16,13 +16,18 @@
|
||||
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { createRouteRef } from '../routing';
|
||||
import { createPlugin } from '../plugin';
|
||||
import { createPlugin } from './Plugin';
|
||||
import {
|
||||
createRoutableExtension,
|
||||
createComponentExtension,
|
||||
} from '../extensions';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import { collectPlugins } from './collection';
|
||||
import {
|
||||
traverseElementTree,
|
||||
childDiscoverer,
|
||||
routeElementDiscoverer,
|
||||
} from '../extensions/traversal';
|
||||
import { pluginCollector } from './collectors';
|
||||
|
||||
const mockConfig = () => ({ path: '/foo', title: 'Foo' });
|
||||
const MockComponent = ({ children }: PropsWithChildren<{}>) => <>{children}</>;
|
||||
@@ -80,6 +85,14 @@ describe('collection', () => {
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(collectPlugins(root)).toEqual(new Set([pluginA, pluginB, pluginC]));
|
||||
const { plugins } = traverseElementTree({
|
||||
root,
|
||||
discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
collectors: {
|
||||
plugins: pluginCollector,
|
||||
},
|
||||
});
|
||||
|
||||
expect(plugins).toEqual(new Set([pluginA, pluginB, pluginC]));
|
||||
});
|
||||
});
|
||||
+9
-37
@@ -29,44 +29,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { isValidElement, ReactNode } from 'react';
|
||||
import { BackstagePlugin } from './types';
|
||||
import { getComponentData } from '../extensions';
|
||||
import { createCollector } from '../extensions/traversal';
|
||||
|
||||
export const collectPlugins = (tree: ReactNode) => {
|
||||
const plugins = new Set<BackstagePlugin>();
|
||||
|
||||
const nodes = [tree];
|
||||
|
||||
while (nodes.length !== 0) {
|
||||
const node = nodes.shift();
|
||||
if (!isIterableElement(node)) {
|
||||
continue;
|
||||
export const pluginCollector = createCollector(
|
||||
new Set<BackstagePlugin>(),
|
||||
(acc, node) => {
|
||||
const plugin = getComponentData<BackstagePlugin>(node, 'core.plugin');
|
||||
if (plugin) {
|
||||
acc.add(plugin);
|
||||
}
|
||||
|
||||
React.Children.forEach(node, child => {
|
||||
if (!isIterableElement(child)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element, children } = child.props as {
|
||||
element?: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
const plugin = getComponentData<BackstagePlugin>(child, 'core.plugin');
|
||||
if (plugin) {
|
||||
plugins.add(plugin);
|
||||
}
|
||||
if (isIterableElement(element)) {
|
||||
nodes.push(element);
|
||||
}
|
||||
nodes.push(children);
|
||||
});
|
||||
}
|
||||
|
||||
return plugins;
|
||||
};
|
||||
|
||||
function isIterableElement(node: ReactNode): node is JSX.Element {
|
||||
return isValidElement(node) || Array.isArray(node);
|
||||
}
|
||||
},
|
||||
);
|
||||
+3
-3
@@ -15,13 +15,13 @@
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { routeCollector, routeParentCollector } from './collectors';
|
||||
|
||||
import {
|
||||
traverseElementTree,
|
||||
childDiscoverer,
|
||||
routeElementDiscoverer,
|
||||
routeCollector,
|
||||
routeParentCollector,
|
||||
} from './discovery';
|
||||
} from '../extensions/traversal';
|
||||
import { createRouteRef } from './RouteRef';
|
||||
import { createPlugin } from '../plugin';
|
||||
import { createRoutableExtension } from '../extensions';
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isValidElement, ReactNode } from 'react';
|
||||
import { RouteRef } from '../routing/types';
|
||||
import { getComponentData } from '../extensions';
|
||||
import { createCollector } from '../extensions/traversal';
|
||||
|
||||
export const routeCollector = createCollector(
|
||||
new Map<RouteRef, string>(),
|
||||
(acc, node, parent) => {
|
||||
if (parent.props.element === node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path: string | undefined = node.props?.path;
|
||||
const element: ReactNode = node.props?.element;
|
||||
|
||||
const routeRef = getComponentData<RouteRef>(node, 'core.mountPoint');
|
||||
if (routeRef) {
|
||||
if (!path) {
|
||||
throw new Error('Mounted routable extension must have a path');
|
||||
}
|
||||
acc.set(routeRef, path);
|
||||
} else if (isValidElement(element)) {
|
||||
const elementRouteRef = getComponentData<RouteRef>(
|
||||
element,
|
||||
'core.mountPoint',
|
||||
);
|
||||
if (elementRouteRef) {
|
||||
if (!path) {
|
||||
throw new Error('Route element must have a path');
|
||||
}
|
||||
acc.set(elementRouteRef, path);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const routeParentCollector = createCollector(
|
||||
new Map<RouteRef, RouteRef | undefined>(),
|
||||
(acc, node, parent, parentRouteRef?: RouteRef) => {
|
||||
if (parent.props.element === node) {
|
||||
return parentRouteRef;
|
||||
}
|
||||
|
||||
const element: ReactNode = node.props?.element;
|
||||
|
||||
let nextParent = parentRouteRef;
|
||||
|
||||
const routeRef = getComponentData<RouteRef>(node, 'core.mountPoint');
|
||||
if (routeRef) {
|
||||
acc.set(routeRef, parentRouteRef);
|
||||
nextParent = routeRef;
|
||||
} else if (isValidElement(element)) {
|
||||
const elementRouteRef = getComponentData<RouteRef>(
|
||||
element,
|
||||
'core.mountPoint',
|
||||
);
|
||||
|
||||
if (elementRouteRef) {
|
||||
acc.set(elementRouteRef, parentRouteRef);
|
||||
nextParent = elementRouteRef;
|
||||
}
|
||||
}
|
||||
|
||||
return nextParent;
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user