diff --git a/packages/core-api/src/extensions/traversal.test.tsx b/packages/core-api/src/extensions/traversal.test.tsx
new file mode 100644
index 0000000000..a69ee9de77
--- /dev/null
+++ b/packages/core-api/src/extensions/traversal.test.tsx
@@ -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 = (
+
+
+
+
+
Title
+ Text
+
+
+ );
+
+ const { names } = traverseElementTree({
+ root,
+ discoverers: [childDiscoverer],
+ collectors: {
+ names: createCollector(Array(), (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 = (
+
+
+
+
+
Title
+ Text
+
+
+ );
+
+ const { names } = traverseElementTree({
+ root,
+ discoverers: [
+ el =>
+ Children.toArray(el.props.children).flatMap(child =>
+ isValidElement(child) ? child?.props?.children : [],
+ ),
+ ],
+ collectors: {
+ names: createCollector(Array(), (acc, el) => {
+ if (typeof el.type === 'string') {
+ acc.push(el.type);
+ }
+ }),
+ },
+ });
+
+ expect(names).toEqual(['main', 'h1', 'p', 'h2', 'span']);
+ });
+});
diff --git a/packages/core-api/src/routing/discovery.tsx b/packages/core-api/src/extensions/traversal.ts
similarity index 68%
rename from packages/core-api/src/routing/discovery.tsx
rename to packages/core-api/src/extensions/traversal.ts
index 0189ae3a4e..a1fbdab25a 100644
--- a/packages/core-api/src/routing/discovery.tsx
+++ b/packages/core-api/src/extensions/traversal.ts
@@ -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 = () => {
+export type Collector = () => {
accumulator: Result;
visit(
accumulator: Result,
@@ -121,6 +119,13 @@ export function traverseElementTree(options: {
) as Results;
}
+export function createCollector(
+ initialResult: Result,
+ visit: ReturnType>['visit'],
+): Collector {
+ 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(
- initialResult: Result,
- visit: ReturnType>['visit'],
-): Collector {
- return () => ({ accumulator: initialResult, visit });
-}
-
-export const routeCollector = createCollector(
- new Map(),
- (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(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(
- 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(),
- (acc, node, parent, parentRouteRef?: RouteRef) => {
- if (parent.props.element === node) {
- return parentRouteRef;
- }
-
- const element: ReactNode = node.props?.element;
-
- let nextParent = parentRouteRef;
-
- const routeRef = getComponentData(node, 'core.mountPoint');
- if (routeRef) {
- acc.set(routeRef, parentRouteRef);
- nextParent = routeRef;
- } else if (isValidElement(element)) {
- const elementRouteRef = getComponentData(
- element,
- 'core.mountPoint',
- );
-
- if (elementRouteRef) {
- acc.set(elementRouteRef, parentRouteRef);
- nextParent = elementRouteRef;
- }
- }
-
- return nextParent;
- },
-);
diff --git a/packages/core-api/src/plugin/collection.test.tsx b/packages/core-api/src/plugin/collectors.test.tsx
similarity index 84%
rename from packages/core-api/src/plugin/collection.test.tsx
rename to packages/core-api/src/plugin/collectors.test.tsx
index fd94303ef4..f040cfd433 100644
--- a/packages/core-api/src/plugin/collection.test.tsx
+++ b/packages/core-api/src/plugin/collectors.test.tsx
@@ -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', () => {
);
- 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]));
});
});
diff --git a/packages/core-api/src/plugin/collection.ts b/packages/core-api/src/plugin/collectors.ts
similarity index 58%
rename from packages/core-api/src/plugin/collection.ts
rename to packages/core-api/src/plugin/collectors.ts
index 0811f66445..a222746f9e 100644
--- a/packages/core-api/src/plugin/collection.ts
+++ b/packages/core-api/src/plugin/collectors.ts
@@ -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();
-
- const nodes = [tree];
-
- while (nodes.length !== 0) {
- const node = nodes.shift();
- if (!isIterableElement(node)) {
- continue;
+export const pluginCollector = createCollector(
+ new Set(),
+ (acc, node) => {
+ const plugin = getComponentData(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(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);
-}
+ },
+);
diff --git a/packages/core-api/src/routing/discovery.test.tsx b/packages/core-api/src/routing/collectors.test.tsx
similarity index 98%
rename from packages/core-api/src/routing/discovery.test.tsx
rename to packages/core-api/src/routing/collectors.test.tsx
index c702811529..c2eefa7e82 100644
--- a/packages/core-api/src/routing/discovery.test.tsx
+++ b/packages/core-api/src/routing/collectors.test.tsx
@@ -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';
diff --git a/packages/core-api/src/routing/collectors.tsx b/packages/core-api/src/routing/collectors.tsx
new file mode 100644
index 0000000000..fab0cbe112
--- /dev/null
+++ b/packages/core-api/src/routing/collectors.tsx
@@ -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(),
+ (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(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(
+ 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(),
+ (acc, node, parent, parentRouteRef?: RouteRef) => {
+ if (parent.props.element === node) {
+ return parentRouteRef;
+ }
+
+ const element: ReactNode = node.props?.element;
+
+ let nextParent = parentRouteRef;
+
+ const routeRef = getComponentData(node, 'core.mountPoint');
+ if (routeRef) {
+ acc.set(routeRef, parentRouteRef);
+ nextParent = routeRef;
+ } else if (isValidElement(element)) {
+ const elementRouteRef = getComponentData(
+ element,
+ 'core.mountPoint',
+ );
+
+ if (elementRouteRef) {
+ acc.set(elementRouteRef, parentRouteRef);
+ nextParent = elementRouteRef;
+ }
+ }
+
+ return nextParent;
+ },
+);