From c7b1c0385a757cc0c03302202633014b03df8f9f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 11 Jul 2022 16:54:51 +0200 Subject: [PATCH] wip Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- packages/app/package.json | 2 + packages/app/src/App.tsx | 50 +- packages/core-app-api/src/app/AppManager.tsx | 5 +- .../src/routing/collectors.test.tsx | 1470 +++++++++-------- .../core-app-api/src/routing/collectors.tsx | 6 +- yarn.lock | 17 + 6 files changed, 800 insertions(+), 750 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index c96797dac4..04e44093d4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -76,6 +76,8 @@ "react-dom": "^17.0.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", + "react-router-stable": "npm:react-router@^6.3.0", + "react-router-dom-stable": "npm:react-router-dom@^6.3.0", "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 9e47ec9ed5..f14e6d1680 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -145,20 +145,20 @@ const AppRouter = app.getRouter(); const routes = ( - } /> + } /> {/* TODO(rubenl): Move this to / once its more mature and components exist */} - }> + }> {homePage} - } /> + } /> } > {entityPage} @@ -166,7 +166,7 @@ const routes = ( } /> } /> - } /> + } /> } > {techDocsPage} @@ -200,7 +200,7 @@ const routes = ( - } /> + } /> } /> - } /> - } /> - } /> - } /> - } /> - }> + } /> + } /> + } /> + } /> + } /> + }> {searchPage} - } /> + } /> } /> } /> - }> - + }> + - } /> - } /> + } /> + } /> ); diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index f36d37f796..434a29246a 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -58,6 +58,7 @@ import { pluginCollector } from '../plugins/collectors'; import { featureFlagCollector, routingV1Collector, + routingV2Collector, } from '../routing/collectors'; import { RoutingProvider } from '../routing/RoutingProvider'; import { RouteTracker } from '../routing/RouteTracker'; @@ -221,9 +222,9 @@ export class AppManager implements BackstageApp { const { routing, featureFlags, routeBindings } = useMemo(() => { const result = traverseElementTree({ root: children, - discoverers: [childDiscoverer, routeElementDiscoverer], + discoverers: [childDiscoverer], collectors: { - routing: routingV1Collector, + routing: routingV2Collector, collectedPlugins: pluginCollector, featureFlags: featureFlagCollector, }, diff --git a/packages/core-app-api/src/routing/collectors.test.tsx b/packages/core-app-api/src/routing/collectors.test.tsx index f22c8d91a6..08f5a3bc35 100644 --- a/packages/core-app-api/src/routing/collectors.test.tsx +++ b/packages/core-app-api/src/routing/collectors.test.tsx @@ -14,399 +14,463 @@ * limitations under the License. */ -import React, { PropsWithChildren } from 'react'; -import { routingV1Collector, routingV2Collector } from './collectors'; +import React from 'react'; +import type { PropsWithChildren } from 'react'; +import type { RouteRef, BackstagePlugin } from '@backstage/core-plugin-api'; -import { - traverseElementTree, - childDiscoverer, - routeElementDiscoverer, -} from '../extensions/traversal'; -import { - createRoutableExtension, - createRouteRef, - createPlugin, - RouteRef, - attachComponentData, - BackstagePlugin, -} from '@backstage/core-plugin-api'; -import { MemoryRouter, Routes, Route } from 'react-router-dom'; +describe.each(['beta', 'stable'])('react-router %s', v => { + beforeAll(() => { + jest.doMock('react-router', () => + v === 'beta' + ? jest.requireActual('react-router') + : jest.requireActual('react-router-stable'), + ); + jest.doMock('react-router-dom', () => + v === 'beta' + ? jest.requireActual('react-router-dom') + : jest.requireActual('react-router-dom-stable'), + ); + }); -const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( - <>{children} -); + function requireDeps() { + return { + ...(require('./collectors') as typeof import('./collectors')), + ...(require('../extensions/traversal') as typeof import('../extensions/traversal')), + ...(require('@backstage/core-plugin-api') as typeof import('@backstage/core-plugin-api')), + ...(require('react-router-dom') as typeof import('react-router-dom')), + }; + } -const plugin = createPlugin({ id: 'my-plugin' }); + afterAll(() => { + jest.resetModules(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); -const ref1 = createRouteRef({ id: 'ref1' }); -const ref2 = createRouteRef({ id: 'ref2' }); -const ref3 = createRouteRef({ id: 'ref3' }); -const ref4 = createRouteRef({ id: 'ref4' }); -const ref5 = createRouteRef({ id: 'ref5' }); -const refOrder = [ref1, ref2, ref3, ref4, ref5]; + it('does something', () => { + const { Route } = requireDeps(); + console.log('DEBUG: Route =', Route); + }); -const Extension1 = plugin.provide( - createRoutableExtension({ - name: 'Extension1', - component: () => Promise.resolve(MockComponent), - mountPoint: ref1, - }), -); -const Extension2 = plugin.provide( - createRoutableExtension({ - name: 'Extension2', - component: () => Promise.resolve(MockComponent), - mountPoint: ref2, - }), -); -const Extension3 = plugin.provide( - createRoutableExtension({ - name: 'Extension3', - component: () => Promise.resolve(MockComponent), - mountPoint: ref3, - }), -); -const Extension4 = plugin.provide( - createRoutableExtension({ - name: 'Extension4', - component: () => Promise.resolve(MockComponent), - mountPoint: ref4, - }), -); -const Extension5 = plugin.provide( - createRoutableExtension({ - name: 'Extension5', - component: () => Promise.resolve(MockComponent), - mountPoint: ref5, - }), -); + const MockComponent = ({ + children, + }: PropsWithChildren<{ path?: string }>) => <>{children}; -const AggregationComponent = ({ - children, -}: PropsWithChildren<{ - path: string; -}>) => <>{children}; + const plugin = createPlugin({ id: 'my-plugin' }); -attachComponentData(AggregationComponent, 'core.gatherMountPoints', true); + const ref1 = createRouteRef({ id: 'ref1' }); + const ref2 = createRouteRef({ id: 'ref2' }); + const ref3 = createRouteRef({ id: 'ref3' }); + const ref4 = createRouteRef({ id: 'ref4' }); + const ref5 = createRouteRef({ id: 'ref5' }); + const refOrder = [ref1, ref2, ref3, ref4, ref5]; -function sortedEntries(map: Map): [RouteRef, T][] { - return Array.from(map).sort( - ([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b), + const Extension1 = plugin.provide( + createRoutableExtension({ + name: 'Extension1', + component: () => Promise.resolve(MockComponent), + mountPoint: ref1, + }), + ); + const Extension2 = plugin.provide( + createRoutableExtension({ + name: 'Extension2', + component: () => Promise.resolve(MockComponent), + mountPoint: ref2, + }), + ); + const Extension3 = plugin.provide( + createRoutableExtension({ + name: 'Extension3', + component: () => Promise.resolve(MockComponent), + mountPoint: ref3, + }), + ); + const Extension4 = plugin.provide( + createRoutableExtension({ + name: 'Extension4', + component: () => Promise.resolve(MockComponent), + mountPoint: ref4, + }), + ); + const Extension5 = plugin.provide( + createRoutableExtension({ + name: 'Extension5', + component: () => Promise.resolve(MockComponent), + mountPoint: ref5, + }), ); -} -function routeObj( - path: string, - refs: RouteRef[], - children: any[] = [], - type: 'mounted' | 'gathered' = 'mounted', - backstagePlugin?: BackstagePlugin, -) { - return { - path: path, - caseSensitive: false, - element: type, - routeRefs: new Set(refs), - children: [ - { - path: '*', - caseSensitive: false, - element: 'match-all', - routeRefs: new Set(), - }, - ...children, - ], - plugin: backstagePlugin, - }; -} + const AggregationComponent = ({ + children, + }: PropsWithChildren<{ + path: string; + }>) => <>{children}; -describe('routingV1Collector', () => { - it('should collect routes', () => { - const list = [ -
, -
, -
- -
, - ]; + attachComponentData(AggregationComponent, 'core.gatherMountPoints', true); - const root = ( - - - + function sortedEntries(map: Map): [RouteRef, T][] { + return Array.from(map).sort( + ([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b), + ); + } + + function routeObj( + path: string, + refs: RouteRef[], + children: any[] = [], + type: 'mounted' | 'gathered' = 'mounted', + backstagePlugin?: BackstagePlugin, + ) { + return { + path: path, + caseSensitive: false, + element: type, + routeRefs: new Set(refs), + children: [ + { + path: '*', + caseSensitive: false, + element: 'match-all', + routeRefs: new Set(), + }, + ...children, + ], + plugin: backstagePlugin, + }; + } + + describe('routingV1Collector', () => { + it('should collect routes', () => { + const list = [ +
, +
, +
+ +
, + ]; + + const root = ( + + + +
+ +
+
+ Some text here shouldn't be a problem +
+ {null} +
+ +
+ + {false} + {list} + {true} + {0} +
+
- -
-
- Some text here shouldn't be a problem -
- {null} -
- -
- - {false} - {list} - {true} - {0} + } />
- -
- } /> -
- - - ); + + + ); - const { routing } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV1Collector, - }, - }); - expect(sortedEntries(routing.paths)).toEqual([ - [ref1, '/foo'], - [ref2, '/bar/:id'], - [ref3, '/baz'], - [ref4, '/divsoup'], - [ref5, '/blop'], - ]); - expect(sortedEntries(routing.parents)).toEqual([ - [ref1, undefined], - [ref2, ref1], - [ref3, ref2], - [ref4, undefined], - [ref5, ref1], - ]); - expect(routing.objects).toEqual([ - routeObj( - '/foo', - [ref1], - [ - routeObj('/bar/:id', [ref2], [routeObj('/baz', [ref3])]), - routeObj('/blop', [ref5]), - ], - ), - routeObj('/divsoup', [ref4], undefined, undefined, plugin), - ]); - }); - - it('should handle all react router Route patterns', () => { - const root = ( - - - - - - - - } - /> - }> - } /> - - - - - ); - - const { routing } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV1Collector, - }, - }); - expect(sortedEntries(routing.paths)).toEqual([ - [ref1, '/foo'], - [ref2, '/bar/:id'], - [ref3, '/baz'], - [ref4, '/divsoup'], - [ref5, '/blop'], - ]); - expect(sortedEntries(routing.parents)).toEqual([ - [ref1, undefined], - [ref2, ref1], - [ref3, undefined], - [ref4, ref3], - [ref5, ref3], - ]); - }); - - it('should use the route aggregator key to bind child routes to the same path', () => { - const root = ( - - - - -
- -
- HELLO -
- - - - - - - -
-
- ); - - const { routing } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV1Collector, - }, - }); - expect(sortedEntries(routing.paths)).toEqual([ - [ref1, '/foo'], - [ref2, '/foo'], - [ref3, '/bar'], - [ref4, '/baz'], - [ref5, '/baz'], - ]); - expect(sortedEntries(routing.parents)).toEqual([ - [ref1, undefined], - [ref2, undefined], - [ref3, undefined], - [ref4, ref3], - [ref5, ref3], - ]); - expect(routing.objects).toEqual([ - routeObj('/foo', [ref1, ref2], [], 'gathered'), - routeObj( - '/bar', - [ref3], - [routeObj('/baz', [ref4, ref5], [], 'gathered')], - ), - ]); - }); - - it('should use the route aggregator but stop when encountering explicit path', () => { - const root = ( - - - - - - - - - - - - - - - ); - - const { routing } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV1Collector, - }, - }); - expect(sortedEntries(routing.paths)).toEqual([ - [ref1, '/foo'], - [ref2, '/bar'], - [ref3, '/baz'], - [ref4, '/blop'], - [ref5, '/bar'], - ]); - expect(sortedEntries(routing.parents)).toEqual([ - [ref1, undefined], - [ref2, ref1], - [ref3, ref1], - [ref4, ref3], - [ref5, ref1], - ]); - expect(routing.objects).toEqual([ - routeObj( - '/foo', - [ref1], - [ - routeObj( - '/bar', - [ref2, ref5], - [routeObj('/baz', [ref3], [routeObj('/blop', [ref4])])], - 'gathered', - ), - ], - ), - ]); - }); - - it('should stop gathering mount points after encountering explicit path', () => { - const root = ( - - - - - - - - - - - - ); - - expect(() => { - traverseElementTree({ + const { routing } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { routing: routingV1Collector, }, }); - }).toThrow('Mounted routable extension must have a path'); - }); -}); - -describe('routingV2Collector', () => { - function routeRoot(element: JSX.Element) { - return ( - - {element} - - ); - } - - it('should associate path with extension in element prop', () => { - const { routing } = traverseElementTree({ - root: routeRoot(} />), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar/:id'], + [ref3, '/baz'], + [ref4, '/divsoup'], + [ref5, '/blop'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, ref2], + [ref4, undefined], + [ref5, ref1], + ]); + expect(routing.objects).toEqual([ + routeObj( + '/foo', + [ref1], + [ + routeObj('/bar/:id', [ref2], [routeObj('/baz', [ref3])]), + routeObj('/blop', [ref5]), + ], + ), + routeObj('/divsoup', [ref4], undefined, undefined, plugin), + ]); }); - expect(sortedEntries(routing.paths)).toEqual([[ref1, '/foo']]); - expect(sortedEntries(routing.parents)).toEqual([[ref1, undefined]]); - expect(routing.objects).toEqual([ - routeObj('/foo', [ref1], [], undefined, plugin), - ]); + it('should handle all react router Route patterns', () => { + const root = ( + + + + + + + + } + /> + }> + } /> + + + + + ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar/:id'], + [ref3, '/baz'], + [ref4, '/divsoup'], + [ref5, '/blop'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, undefined], + [ref4, ref3], + [ref5, ref3], + ]); + }); + + it('should use the route aggregator key to bind child routes to the same path', () => { + const root = ( + + + + +
+ +
+ HELLO +
+ + + + + + + +
+
+ ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/foo'], + [ref3, '/bar'], + [ref4, '/baz'], + [ref5, '/baz'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, undefined], + [ref3, undefined], + [ref4, ref3], + [ref5, ref3], + ]); + expect(routing.objects).toEqual([ + routeObj('/foo', [ref1, ref2], [], 'gathered'), + routeObj( + '/bar', + [ref3], + [routeObj('/baz', [ref4, ref5], [], 'gathered')], + ), + ]); + }); + + it('should use the route aggregator but stop when encountering explicit path', () => { + const root = ( + + + + + + + + + + + + + + + ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar'], + [ref3, '/baz'], + [ref4, '/blop'], + [ref5, '/bar'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, ref1], + [ref4, ref3], + [ref5, ref1], + ]); + expect(routing.objects).toEqual([ + routeObj( + '/foo', + [ref1], + [ + routeObj( + '/bar', + [ref2, ref5], + [routeObj('/baz', [ref3], [routeObj('/blop', [ref4])])], + 'gathered', + ), + ], + ), + ]); + }); + + it('should stop gathering mount points after encountering explicit path', () => { + const root = ( + + + + + + + + + + + + ); + + expect(() => { + traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + }).toThrow('Mounted routable extension must have a path'); + }); }); - it('should not allow multiple extensions within the same element prop', () => { - expect(() => - traverseElementTree({ + describe('routingV2Collector', () => { + function routeRoot(element: JSX.Element) { + return ( + + {element} + + ); + } + + it('should associate path with extension in element prop', () => { + const { routing } = traverseElementTree({ + root: routeRoot(} />), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }); + + expect(sortedEntries(routing.paths)).toEqual([[ref1, '/foo']]); + expect(sortedEntries(routing.parents)).toEqual([[ref1, undefined]]); + expect(routing.objects).toEqual([ + routeObj('/foo', [ref1], [], undefined, plugin), + ]); + }); + + it('should not allow multiple extensions within the same element prop', () => { + expect(() => + traverseElementTree({ + root: routeRoot( + + + + + } + />, + ), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }), + ).toThrow('Route element may not contain multiple routable extensions'); + }); + + it('should not support inline path', () => { + expect(() => + traverseElementTree({ + root: routeRoot(), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }), + ).toThrow( + 'Path property may not be set directly on a routable extension', + ); + }); + + it('should associate the path with extensions deep in the element prop', () => { + const { routing } = traverseElementTree({ root: routeRoot( - - +
+ + {[ + undefined, + null, +
+ +
, + ]} +
} />, @@ -415,389 +479,351 @@ describe('routingV2Collector', () => { collectors: { routing: routingV2Collector, }, - }), - ).toThrow('Route element may not contain multiple routable extensions'); - }); + }); - it('should not support inline path', () => { - expect(() => - traverseElementTree({ - root: routeRoot(), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, - }), - ).toThrow('Path property may not be set directly on a routable extension'); - }); - - it('should associate the path with extensions deep in the element prop', () => { - const { routing } = traverseElementTree({ - root: routeRoot( - -
- - {[ - undefined, - null, -
- -
, - ]} -
- - } - />, - ), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, + expect(sortedEntries(routing.paths)).toEqual([[ref1, '/foo']]); + expect(sortedEntries(routing.parents)).toEqual([[ref1, undefined]]); + expect(routing.objects).toEqual([ + routeObj('/foo', [ref1], [], undefined, plugin), + ]); }); - expect(sortedEntries(routing.paths)).toEqual([[ref1, '/foo']]); - expect(sortedEntries(routing.parents)).toEqual([[ref1, undefined]]); - expect(routing.objects).toEqual([ - routeObj('/foo', [ref1], [], undefined, plugin), - ]); - }); + it('should not associate path with extension in children prop', () => { + expect(() => + traverseElementTree({ + root: routeRoot( + + + , + ), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }), + ).toThrow('Routable extension must be assigned a path'); + }); - it('should not associate path with extension in children prop', () => { - expect(() => - traverseElementTree({ + it('should assign parent for extension in element prop', () => { + const { routing } = traverseElementTree({ root: routeRoot( - - + }> + } /> , ), discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { routing: routingV2Collector, }, - }), - ).toThrow('Routable extension must be assigned a path'); - }); - - it('should assign parent for extension in element prop', () => { - const { routing } = traverseElementTree({ - root: routeRoot( - }> - } /> - , - ), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, - }); - - expect(sortedEntries(routing.paths)).toEqual([ - [ref1, '/foo'], - [ref2, '/bar'], - ]); - expect(sortedEntries(routing.parents)).toEqual([ - [ref1, undefined], - [ref2, ref1], - ]); - expect(routing.objects).toEqual([ - routeObj( - '/foo', - [ref1], - [routeObj('/bar', [ref2], [], undefined, plugin)], - undefined, - plugin, - ), - ]); - }); - - it('should not allow paths within element props', () => { - expect(() => { - traverseElementTree({ - root: routeRoot( - } />} - />, - ), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, }); - }).toThrow('Elements within the element prop tree may not contain paths'); - }); - it('should not allow extensions within the element prop to have path props either', () => { - expect(() => { - traverseElementTree({ - root: routeRoot( - } />, + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + ]); + expect(routing.objects).toEqual([ + routeObj( + '/foo', + [ref1], + [routeObj('/bar', [ref2], [], undefined, plugin)], + undefined, + plugin, ), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, - }); - }).toThrow('Elements within the element prop tree may not contain paths'); - }); - - it('should gather extension', () => { - const { routing } = traverseElementTree({ - root: routeRoot( - - - , - ), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, + ]); }); - expect(sortedEntries(routing.paths)).toEqual([[ref1, '/foo']]); - expect(sortedEntries(routing.parents)).toEqual([[ref1, undefined]]); - expect(routing.objects).toEqual([routeObj('/foo', [ref1], [], 'gathered')]); - }); - - it('should reset gathering if path prop is encountered', () => { - const { routing } = traverseElementTree({ - root: routeRoot( - - - } /> - - , - ), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, - }); - - expect(sortedEntries(routing.paths)).toEqual([ - [ref1, '/foo'], - [ref2, '/bar'], - ]); - expect(sortedEntries(routing.parents)).toEqual([ - [ref1, undefined], - [ref2, ref1], - ]); - expect(routing.objects).toEqual([ - routeObj( - '/foo', - [ref1], - [routeObj('/bar', [ref2], [], undefined, plugin)], - 'gathered', - ), - ]); - }); - - it('should collect routes defined with different patterns', () => { - const list = [ -
, -
, -
- } /> -
, - ]; - - const { routing } = traverseElementTree({ - root: routeRoot( - <> - }> -
- }> -
-
- Some text here shouldn't be a problem -
- {null} -
- } /> -
- - {false} - {list} - {true} - {0} -
- -
- } /> -
- , - ), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, - }); - expect(sortedEntries(routing.paths)).toEqual([ - [ref1, '/foo'], - [ref2, '/bar/:id'], - [ref3, '/baz'], - [ref4, '/divsoup'], - [ref5, '/blop'], - ]); - expect(sortedEntries(routing.parents)).toEqual([ - [ref1, undefined], - [ref2, ref1], - [ref3, ref2], - [ref4, undefined], - [ref5, ref1], - ]); - expect(routing.objects).toEqual([ - routeObj( - '/foo', - [ref1], - [ - routeObj( - '/bar/:id', - [ref2], - [routeObj('/baz', [ref3], [], undefined, plugin)], - undefined, - plugin, + it('should not allow paths within element props', () => { + expect(() => { + traverseElementTree({ + root: routeRoot( + } />} + />, ), - routeObj('/blop', [ref5], [], undefined, plugin), - ], - undefined, - plugin, - ), - routeObj('/divsoup', [ref4], undefined, undefined, plugin), - ]); - }); + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }); + }).toThrow('Elements within the element prop tree may not contain paths'); + }); - it('should handle a subset of react router Route patterns', () => { - const { routing } = traverseElementTree({ - root: routeRoot( - <> - } /> - { + expect(() => { + traverseElementTree({ + root: routeRoot( + } />, + ), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }); + }).toThrow('Elements within the element prop tree may not contain paths'); + }); + + it('should gather extension', () => { + const { routing } = traverseElementTree({ + root: routeRoot( + + + , + ), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }); + + expect(sortedEntries(routing.paths)).toEqual([[ref1, '/foo']]); + expect(sortedEntries(routing.parents)).toEqual([[ref1, undefined]]); + expect(routing.objects).toEqual([ + routeObj('/foo', [ref1], [], 'gathered'), + ]); + }); + + it('should reset gathering if path prop is encountered', () => { + const { routing } = traverseElementTree({ + root: routeRoot( + + + } /> + + , + ), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }); + + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + ]); + expect(routing.objects).toEqual([ + routeObj( + '/foo', + [ref1], + [routeObj('/bar', [ref2], [], undefined, plugin)], + 'gathered', + ), + ]); + }); + + it('should collect routes defined with different patterns', () => { + const list = [ +
, +
, +
+ } /> +
, + ]; + + const { routing } = traverseElementTree({ + root: routeRoot( + <> + }> +
+ }> +
+
+ Some text here shouldn't be a problem +
+ {null} +
+ } /> +
+ + {false} + {list} + {true} + {0} +
+ +
+ } /> +
+ , + ), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar/:id'], + [ref3, '/baz'], + [ref4, '/divsoup'], + [ref5, '/blop'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, ref2], + [ref4, undefined], + [ref5, ref1], + ]); + expect(routing.objects).toEqual([ + routeObj( + '/foo', + [ref1], + [ + routeObj( + '/bar/:id', + [ref2], + [routeObj('/baz', [ref3], [], undefined, plugin)], + undefined, + plugin, + ), + routeObj('/blop', [ref5], [], undefined, plugin), + ], + undefined, + plugin, + ), + routeObj('/divsoup', [ref4], undefined, undefined, plugin), + ]); + }); + + it('should handle a subset of react router Route patterns', () => { + const { routing } = traverseElementTree({ + root: routeRoot( + <> + } /> + + +
+ } + > + } /> + + + + + , + ), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/baz'], + [ref3, '/child'], + [ref4, '/collected'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, undefined], + [ref3, ref2], + [ref4, ref2], + ]); + }); + + it('should bind child routes to the same path with a gatherer flag', () => { + const { routing } = traverseElementTree({ + root: routeRoot( + <> + +
- } - > - } /> - - + HELLO - - , - ), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, + , + ), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/foo'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, undefined], + ]); + expect(routing.objects).toEqual([ + routeObj('/foo', [ref1, ref2], [], 'gathered'), + ]); }); - expect(sortedEntries(routing.paths)).toEqual([ - [ref1, '/foo'], - [ref2, '/baz'], - [ref3, '/child'], - [ref4, '/collected'], - ]); - expect(sortedEntries(routing.parents)).toEqual([ - [ref1, undefined], - [ref2, undefined], - [ref3, ref2], - [ref4, ref2], - ]); - }); - it('should bind child routes to the same path with a gatherer flag', () => { - const { routing } = traverseElementTree({ - root: routeRoot( - <> - - -
- -
- HELLO -
- , - ), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, + it('should gather routes but stop when encountering explicit path', () => { + const { routing } = traverseElementTree({ + root: routeRoot( + }> + + + }> + + + + + + + + , + ), + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV2Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar'], + [ref3, '/baz'], + [ref4, '/blop'], + [ref5, '/bar'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, ref2], + [ref4, ref3], + [ref5, ref1], + ]); + expect(routing.objects).toEqual([ + routeObj( + '/foo', + [ref1], + [ + routeObj( + '/bar', + [ref2, ref5], + [ + routeObj( + '/baz', + [ref3], + [routeObj('/blop', [ref4], [], 'gathered')], + undefined, + plugin, + ), + ], + 'gathered', + ), + ], + undefined, + plugin, + ), + ]); }); - expect(sortedEntries(routing.paths)).toEqual([ - [ref1, '/foo'], - [ref2, '/foo'], - ]); - expect(sortedEntries(routing.parents)).toEqual([ - [ref1, undefined], - [ref2, undefined], - ]); - expect(routing.objects).toEqual([ - routeObj('/foo', [ref1, ref2], [], 'gathered'), - ]); - }); - - it('should gather routes but stop when encountering explicit path', () => { - const { routing } = traverseElementTree({ - root: routeRoot( - }> - - - }> - - - - - - - - , - ), - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV2Collector, - }, - }); - expect(sortedEntries(routing.paths)).toEqual([ - [ref1, '/foo'], - [ref2, '/bar'], - [ref3, '/baz'], - [ref4, '/blop'], - [ref5, '/bar'], - ]); - expect(sortedEntries(routing.parents)).toEqual([ - [ref1, undefined], - [ref2, ref1], - [ref3, ref2], - [ref4, ref3], - [ref5, ref1], - ]); - expect(routing.objects).toEqual([ - routeObj( - '/foo', - [ref1], - [ - routeObj( - '/bar', - [ref2, ref5], - [ - routeObj( - '/baz', - [ref3], - [routeObj('/blop', [ref4], [], 'gathered')], - undefined, - plugin, - ), - ], - 'gathered', - ), - ], - undefined, - plugin, - ), - ]); }); }); diff --git a/packages/core-app-api/src/routing/collectors.tsx b/packages/core-app-api/src/routing/collectors.tsx index e036a6ef3b..895be6afa2 100644 --- a/packages/core-app-api/src/routing/collectors.tsx +++ b/packages/core-app-api/src/routing/collectors.tsx @@ -128,12 +128,16 @@ export const routingV2Collector = createCollector( } if (elementProp) { - const [{ routeRef, plugin }, ...others] = collectSubTree(elementProp); + const [extension, ...others] = collectSubTree(elementProp); if (others.length > 0) { throw new Error( 'Route element may not contain multiple routable extensions', ); } + if (!extension) { + return ctx; + } + const { routeRef, plugin } = extension; const newObj = { path, diff --git a/yarn.lock b/yarn.lock index 962e89a7c0..b8edff8e57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13459,6 +13459,8 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: react-dom "^17.0.2" react-router "6.0.0-beta.0" react-router-dom "6.0.0-beta.0" + react-router-dom-stable "npm:react-router-dom@^6.3.0" + react-router-stable "npm:react-router@^6.3.0" react-use "^17.2.4" zen-observable "^0.8.15" @@ -22400,6 +22402,14 @@ react-resize-detector@^6.6.3: lodash "^4.17.21" resize-observer-polyfill "^1.5.1" +"react-router-dom-stable@npm:react-router-dom@^6.3.0": + version "6.3.0" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" + integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== + dependencies: + history "^5.2.0" + react-router "6.3.0" + react-router-dom@6.0.0-beta.0: version "6.0.0-beta.0" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.0-beta.0.tgz#9dcc8555365f22f7fbd09f26b6b82543f3eb97d6" @@ -22408,6 +22418,13 @@ react-router-dom@6.0.0-beta.0: prop-types "^15.7.2" react-router "6.0.0-beta.0" +"react-router-stable@npm:react-router@^6.3.0", react-router@6.3.0: + version "6.3.0" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" + integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== + dependencies: + history "^5.2.0" + react-router@6.0.0-beta.0: version "6.0.0-beta.0" resolved "https://registry.npmjs.org/react-router/-/react-router-6.0.0-beta.0.tgz#3e11f39b6ded4412c2fed9e4f989dd4c8156724d"