frontend-app-api: partial support for old routing system
Co-authored-by: Camila Belo <camilaibs@gmail.com> Co-authored-by: Philipp Hugenroth <philipph@spotify.com> Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 from 'react';
|
||||
import { RouteRef, createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { extractRouteInfoFromInstanceTree } from './extractRouteInfoFromInstanceTree';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createInstances } from '../wiring/createApp';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
|
||||
const ref1 = createRouteRef({ id: 'page1' });
|
||||
const ref2 = createRouteRef({ id: 'page2' });
|
||||
const ref3 = createRouteRef({ id: 'page3' });
|
||||
const ref4 = createRouteRef({ id: 'page4' });
|
||||
const ref5 = createRouteRef({ id: 'page5' });
|
||||
const refOrder = [ref1, ref2, ref3, ref4, ref5];
|
||||
|
||||
function sortedEntries<T>(map: Map<RouteRef, T>): [RouteRef, T][] {
|
||||
return Array.from(map).sort(
|
||||
([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b),
|
||||
);
|
||||
}
|
||||
|
||||
// async 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(),
|
||||
// plugins: new Set(),
|
||||
// },
|
||||
// ...children,
|
||||
// ],
|
||||
// plugins: backstagePlugin ? new Set([backstagePlugin]) : new Set(),
|
||||
// };
|
||||
// }
|
||||
|
||||
const emptyLoader = () => Promise.resolve(React.createElement('div'));
|
||||
|
||||
describe('discovery', () => {
|
||||
it('should collect routes', () => {
|
||||
const extensions = [
|
||||
createPageExtension({
|
||||
id: 'nothing',
|
||||
defaultPath: 'nothing',
|
||||
loader: emptyLoader,
|
||||
}),
|
||||
createPageExtension({
|
||||
id: 'page1',
|
||||
defaultPath: 'foo',
|
||||
routeRef: ref1,
|
||||
inputs: {
|
||||
routes: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
loader: emptyLoader,
|
||||
}),
|
||||
createPageExtension({
|
||||
id: 'page2',
|
||||
at: 'page1/routes',
|
||||
defaultPath: 'bar/:id',
|
||||
routeRef: ref2,
|
||||
inputs: {
|
||||
routes: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
loader: emptyLoader,
|
||||
}),
|
||||
createPageExtension({
|
||||
id: 'page3',
|
||||
at: 'page2/routes',
|
||||
defaultPath: 'baz',
|
||||
routeRef: ref3,
|
||||
loader: emptyLoader,
|
||||
}),
|
||||
createPageExtension({
|
||||
id: 'page4',
|
||||
defaultPath: 'divsoup',
|
||||
routeRef: ref4,
|
||||
loader: emptyLoader,
|
||||
}),
|
||||
createPageExtension({
|
||||
id: 'page5',
|
||||
at: 'page1/routes',
|
||||
defaultPath: 'blop',
|
||||
routeRef: ref5,
|
||||
loader: emptyLoader,
|
||||
}),
|
||||
];
|
||||
|
||||
const { rootInstances } = createInstances({
|
||||
config: new MockConfigApi({}),
|
||||
plugins: [
|
||||
createPlugin({
|
||||
id: 'test',
|
||||
extensions,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const info = extractRouteInfoFromInstanceTree(rootInstances);
|
||||
|
||||
expect(sortedEntries(info.routePaths)).toEqual([
|
||||
[ref1, 'foo'],
|
||||
[ref2, 'bar/:id'],
|
||||
[ref3, 'baz'],
|
||||
[ref4, 'divsoup'],
|
||||
[ref5, 'blop'],
|
||||
]);
|
||||
expect(sortedEntries(info.routeParents)).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, undefined, plugin)],
|
||||
// undefined,
|
||||
// plugin,
|
||||
// ),
|
||||
// routeObj('blop', [ref5], undefined, undefined, plugin),
|
||||
// ],
|
||||
// undefined,
|
||||
// plugin,
|
||||
// ),
|
||||
// routeObj('divsoup', [ref4], undefined, undefined, plugin),
|
||||
// ]);
|
||||
});
|
||||
|
||||
// it('should handle all react router Route patterns', () => {
|
||||
// const root = (
|
||||
// <MemoryRouter>
|
||||
// <Routes>
|
||||
// <Route path="foo" element={<Extension1 />}>
|
||||
// <Routes>
|
||||
// <Route path="bar/:id" element={<Extension2 />} />
|
||||
// </Routes>
|
||||
// </Route>
|
||||
// <Route path="baz" element={<Extension3 />}>
|
||||
// <Route path="divsoup" element={<Extension4 />} />
|
||||
// <Route path="blop" element={<Extension5 />} />
|
||||
// </Route>
|
||||
// </Routes>
|
||||
// </MemoryRouter>
|
||||
// );
|
||||
|
||||
// const { routing } = traverseElementTree({
|
||||
// root,
|
||||
// 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, undefined],
|
||||
// [ref4, ref3],
|
||||
// [ref5, ref3],
|
||||
// ]);
|
||||
// });
|
||||
|
||||
// it('should handle absolute route paths', () => {
|
||||
// const root = (
|
||||
// <MemoryRouter>
|
||||
// <Routes>
|
||||
// <Route path="/foo" element={<Extension1 />}>
|
||||
// <Routes>
|
||||
// <Route path="/bar/:id" element={<Extension2 />} />
|
||||
// </Routes>
|
||||
// </Route>
|
||||
// <Route path="/baz" element={<Extension3 />}>
|
||||
// <Route path="/divsoup" element={<Extension4 />} />
|
||||
// <Route path="/blop" element={<Extension5 />} />
|
||||
// </Route>
|
||||
// </Routes>
|
||||
// </MemoryRouter>
|
||||
// );
|
||||
|
||||
// const { routing } = traverseElementTree({
|
||||
// root,
|
||||
// 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, undefined],
|
||||
// [ref4, ref3],
|
||||
// [ref5, ref3],
|
||||
// ]);
|
||||
// });
|
||||
|
||||
// it('should use the route aggregator key to bind child routes to the same path', () => {
|
||||
// const root = (
|
||||
// <MemoryRouter>
|
||||
// <Routes>
|
||||
// <AggregationComponent path="foo">
|
||||
// <Extension1 />
|
||||
// <div>
|
||||
// <Extension2 />
|
||||
// </div>
|
||||
// HELLO
|
||||
// </AggregationComponent>
|
||||
// <Route path="bar" element={<Extension3 />}>
|
||||
// <AggregationComponent path="">
|
||||
// <Extension4>
|
||||
// <Extension5 />
|
||||
// </Extension4>
|
||||
// </AggregationComponent>
|
||||
// </Route>
|
||||
// </Routes>
|
||||
// </MemoryRouter>
|
||||
// );
|
||||
|
||||
// const { routing } = traverseElementTree({
|
||||
// root,
|
||||
// discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
// collectors: {
|
||||
// routing: routingV2Collector,
|
||||
// },
|
||||
// });
|
||||
// expect(sortedEntries(routing.paths)).toEqual([
|
||||
// [ref1, 'foo'],
|
||||
// [ref2, 'foo'],
|
||||
// [ref3, 'bar'],
|
||||
// [ref4, ''],
|
||||
// [ref5, ''],
|
||||
// ]);
|
||||
// expect(sortedEntries(routing.parents)).toEqual([
|
||||
// [ref1, undefined],
|
||||
// [ref2, undefined],
|
||||
// [ref3, undefined],
|
||||
// [ref4, ref3],
|
||||
// [ref5, ref3],
|
||||
// ]);
|
||||
// expect(routing.objects).toEqual([
|
||||
// routeObj('foo', [ref1, ref2], [], 'gathered', plugin),
|
||||
// routeObj(
|
||||
// 'bar',
|
||||
// [ref3],
|
||||
// [routeObj('', [ref4, ref5], [], 'gathered', plugin)],
|
||||
// undefined,
|
||||
// plugin,
|
||||
// ),
|
||||
// ]);
|
||||
// });
|
||||
|
||||
// it('should use the route aggregator but stop when encountering explicit path', () => {
|
||||
// const root = (
|
||||
// <MemoryRouter>
|
||||
// <Routes>
|
||||
// <Route path="foo" element={<Extension1 />}>
|
||||
// <AggregationComponent path="/bar">
|
||||
// <Extension2>
|
||||
// <Routes>
|
||||
// <Route path="baz" element={<Extension3 />}>
|
||||
// <Route path="/blop" element={<Extension4 />} />
|
||||
// </Route>
|
||||
// </Routes>
|
||||
// <Extension5 />
|
||||
// </Extension2>
|
||||
// </AggregationComponent>
|
||||
// </Route>
|
||||
// </Routes>
|
||||
// </MemoryRouter>
|
||||
// );
|
||||
|
||||
// const { routing } = traverseElementTree({
|
||||
// root,
|
||||
// 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], undefined, undefined, plugin)],
|
||||
// undefined,
|
||||
// plugin,
|
||||
// ),
|
||||
// ],
|
||||
// 'gathered',
|
||||
// plugin,
|
||||
// ),
|
||||
// ],
|
||||
// undefined,
|
||||
// plugin,
|
||||
// ),
|
||||
// ]);
|
||||
// });
|
||||
|
||||
// it('should throw when you provide path property on an extension', () => {
|
||||
// expect(() => {
|
||||
// traverseElementTree({
|
||||
// root: <Extension1 path="/foo" />,
|
||||
// discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
// collectors: {
|
||||
// routing: routingV2Collector,
|
||||
// },
|
||||
// });
|
||||
// }).toThrow(
|
||||
// 'Path property may not be set directly on a routable extension "Extension(Extension1)"',
|
||||
// );
|
||||
// });
|
||||
|
||||
// it('should throw when element prop is not a string', () => {
|
||||
// const Div = 'div' as unknown as ComponentType<{ path: boolean }>;
|
||||
// expect(() => {
|
||||
// traverseElementTree({
|
||||
// root: <Div path />,
|
||||
// discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
// collectors: {
|
||||
// routing: routingV2Collector,
|
||||
// },
|
||||
// });
|
||||
// }).toThrow('Element path must be a string at "div"');
|
||||
// });
|
||||
|
||||
// it('should throw when the mount point gatherers have an element prop', () => {
|
||||
// const AnyAggregationComponent = AggregationComponent as any;
|
||||
// expect(() => {
|
||||
// traverseElementTree({
|
||||
// root: <AnyAggregationComponent path="test" element={<Extension3 />} />,
|
||||
// discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
// collectors: {
|
||||
// routing: routingV2Collector,
|
||||
// },
|
||||
// });
|
||||
// }).toThrow(
|
||||
// 'Mount point gatherers may not have an element prop "AggregationComponent"',
|
||||
// );
|
||||
// });
|
||||
|
||||
// it('should ignore path props within route elements', () => {
|
||||
// const { routing } = traverseElementTree({
|
||||
// root: <Route path="foo" element={<Extension1 path="bar" />} />,
|
||||
// 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 throw when a routable extension does not have a path set', () => {
|
||||
// expect(() => {
|
||||
// traverseElementTree({
|
||||
// root: <Extension3 />,
|
||||
// discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
// collectors: {
|
||||
// routing: routingV2Collector,
|
||||
// },
|
||||
// });
|
||||
// }).toThrow(
|
||||
// 'Routable extension "Extension(Extension3)" with mount point "routeRef{type=absolute,id=ref3}" must be assigned a path',
|
||||
// );
|
||||
// });
|
||||
|
||||
// it('should throw when Route elements contain multiple routable extensions', () => {
|
||||
// expect(() => {
|
||||
// traverseElementTree({
|
||||
// root: (
|
||||
// <Route
|
||||
// path="foo"
|
||||
// element={
|
||||
// <>
|
||||
// <Extension1 />
|
||||
// <Extension2 />
|
||||
// </>
|
||||
// }
|
||||
// />
|
||||
// ),
|
||||
// discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
// collectors: {
|
||||
// routing: routingV2Collector,
|
||||
// },
|
||||
// });
|
||||
// }).toThrow(
|
||||
// 'Route element with path "foo" may not contain multiple routable extensions',
|
||||
// );
|
||||
// });
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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 { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { coreExtensionData } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionInstance } from '../wiring/createExtensionInstance';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { BackstageRouteObject } from '../../../core-app-api/src/routing/types';
|
||||
|
||||
// Joins a list of paths together, avoiding trailing and duplicate slashes
|
||||
export function joinPaths(...paths: string[]): string {
|
||||
const normalized = paths.join('/').replace(/\/\/+/g, '/');
|
||||
if (normalized !== '/' && normalized.endsWith('/')) {
|
||||
return normalized.slice(0, -1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function extractRouteInfoFromInstanceTree(roots: ExtensionInstance[]): {
|
||||
routePaths: Map<RouteRef, string>;
|
||||
routeParents: Map<RouteRef, RouteRef | undefined>;
|
||||
routeObjects: BackstageRouteObject[];
|
||||
} {
|
||||
const routePaths = new Map<RouteRef, string>();
|
||||
const routeParents = new Map<RouteRef, RouteRef | undefined>();
|
||||
|
||||
function visit(current: ExtensionInstance, parent?: RouteRef) {
|
||||
const routePath = current.getData(coreExtensionData.routePath) ?? '';
|
||||
const routeRef = current.getData(coreExtensionData.routeRef);
|
||||
|
||||
if (routeRef) {
|
||||
const routeRefId = (routeRef as any).id; // TODO: properly
|
||||
if (routeRefId !== current.id) {
|
||||
throw new Error(
|
||||
`Route ref '${routeRefId}' must have the same ID as extension '${current.id}'`,
|
||||
);
|
||||
}
|
||||
routePaths.set(routeRef, routePath);
|
||||
routeParents.set(routeRef, parent);
|
||||
}
|
||||
|
||||
for (const children of current.attachments.values()) {
|
||||
for (const child of children) {
|
||||
visit(child, routeRef ?? parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of roots) {
|
||||
visit(root);
|
||||
}
|
||||
|
||||
return { routePaths, routeParents, routeObjects: [] };
|
||||
}
|
||||
@@ -74,6 +74,8 @@ import { defaultConfigLoaderSync } from '../../../core-app-api/src/app/defaultCo
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBaseUrlConfigs';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { RoutingProvider as LegacyRoutingProvider } from '../../../core-app-api/src/routing/RoutingProvider';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
apis as defaultApis,
|
||||
components as defaultComponents,
|
||||
@@ -82,6 +84,7 @@ import {
|
||||
import { BrowserRouter, Route } from 'react-router-dom';
|
||||
import { SidebarItem } from '@backstage/core-components';
|
||||
import { DarkTheme, LightTheme } from '../extensions/themes';
|
||||
import { extractRouteInfoFromInstanceTree } from '../routing/extractRouteInfoFromInstanceTree';
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionTreeNode {
|
||||
@@ -281,7 +284,7 @@ export function createApp(options: {
|
||||
config,
|
||||
});
|
||||
|
||||
const routePaths = extractRouteInfoFromInstanceTree(rootInstances);
|
||||
const routeInfo = extractRouteInfoFromInstanceTree(rootInstances);
|
||||
|
||||
const coreInstance = rootInstances.find(({ id }) => id === 'core');
|
||||
if (!coreInstance) {
|
||||
@@ -304,10 +307,15 @@ export function createApp(options: {
|
||||
<ApiProvider apis={apiHolder}>
|
||||
<AppContextProvider appContext={appContext}>
|
||||
<AppThemeProvider>
|
||||
<RoutingProvider routePaths={routePaths}>
|
||||
{/* TODO: set base path using the logic from AppRouter */}
|
||||
<BrowserRouter>{rootElements}</BrowserRouter>
|
||||
</RoutingProvider>
|
||||
<LegacyRoutingProvider
|
||||
{...routeInfo}
|
||||
routeBindings={new Map(/* TODO */)}
|
||||
>
|
||||
<RoutingProvider routePaths={routeInfo.routePaths}>
|
||||
{/* TODO: set base path using the logic from AppRouter */}
|
||||
<BrowserRouter>{rootElements}</BrowserRouter>
|
||||
</RoutingProvider>
|
||||
</LegacyRoutingProvider>
|
||||
</AppThemeProvider>
|
||||
</AppContextProvider>
|
||||
</ApiProvider>
|
||||
@@ -458,38 +466,3 @@ function createApiHolder(
|
||||
|
||||
return new ApiResolver(factoryRegistry);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function extractRouteInfoFromInstanceTree(
|
||||
roots: ExtensionInstance[],
|
||||
): Map<RouteRef, string> {
|
||||
const results = new Map<RouteRef, string>();
|
||||
|
||||
function visit(current: ExtensionInstance, basePath: string) {
|
||||
const routePath = current.getData(coreExtensionData.routePath) ?? '';
|
||||
const routeRef = current.getData(coreExtensionData.routeRef);
|
||||
|
||||
// TODO: join paths in a more robust way
|
||||
const fullPath = basePath + routePath;
|
||||
if (routeRef) {
|
||||
const routeRefId = (routeRef as any).id; // TODO: properly
|
||||
if (routeRefId !== current.id) {
|
||||
throw new Error(
|
||||
`Route ref '${routeRefId}' must have the same ID as extension '${current.id}'`,
|
||||
);
|
||||
}
|
||||
results.set(routeRef, fullPath);
|
||||
}
|
||||
|
||||
for (const children of current.attachments.values()) {
|
||||
for (const child of children) {
|
||||
visit(child, fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of roots) {
|
||||
visit(root, '');
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user