diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 49a2c9681c..3d29f19c13 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -1,107 +1,146 @@ app: packages: 'all' # ✨ - extensions: - - graphiql.page: - config: - path: / - # - core.signInPage: - # props: - # provider: - # id: google - # title: Google - # message: Sign In using Google - # apiRef: googleAuthApiRef # ??? - # - core.nav: - # config: - # logo: assets/logo.png - # layout: - # - label: Search - # icon: search - # to: /search - # items: - # - point: search - # - type: divider - # - label: Menu - # icon: menu - # items: - # - label: Home - # icon: home - # to: /catalog - # - label: Create... - # icon: create - # to: /create - # - type: divider - # - type: scroll-wrapper - # items: - # - label: Tech Radar - # icon: map - # to: /tech-radar - # - label: GraphiQL - # icon: graphiql - # to: /graphiql - # - type: divider - # - point: shortcuts - # - type: space - # - type: divider - # - label: Settings - # icon: avatar - # to: /settings - # items: - # - point: settings - # - core.nav/search: '@backstage/plugin-search#SidebarSearchModal' - # - core.nav/shortcuts: - # use: '@backstage/plugin-shortcuts#Shortcuts' - # config: - # allowExternalLinks: true - # - core.nav/settings: '@backstage/plugin-user-settings#SidebarSettings' + # scmAuthExtension: >- + # createScmAuthExtension({ + # id: 'apis.scmAuth.addons.ghe', + # factory({ bind }) { + # bind.scmAuthAddon({ + # baseUrl: 'https://github.spotify.net', + # api: githubAuthApiRef, + # }) + # } + # }) - # - core.pages.index: - # at: - # point: core.routes - # config: - # path: / - # use: 'react-router-dom#Navigate' - # config: - # to: /catalog + # externalRouteRefs: + # bind(catalogPlugin.externalRoutes, { + # createComponent: scaffolderPlugin.routes.root, + # viewTechDoc: techdocsPlugin.routes.docRoot, + # createFromTemplate: scaffolderPlugin.routes.selectedTemplate, + # }); + # bind(apiDocsPlugin.externalRoutes, { + # registerApi: catalogImportPlugin.routes.importPage, + # }); + # bind(scaffolderPlugin.externalRoutes, { + # registerComponent: catalogImportPlugin.routes.importPage, + # viewTechDoc: techdocsPlugin.routes.docRoot, + # }); + # bind(orgPlugin.externalRoutes, { + # catalogIndex: catalogPlugin.routes.catalogIndex, + # }); - # - scaffolder.page: - # config: - # groups: - # - title: Recommended - # filter: - # entity.metadata.tags: recommended - # - scaffolder.page/fields: '@backstage/plugin-scaffolder#LowerCaseValuePickerFieldExtension' - # - scaffolder.page/layout: '@backstage/plugin-scaffolder#TwoColumnLayout' + # extensions: + # - plugin.catalog: + # config: + # externalRoutes: + # createComponent: plugin.scaffolder.page + # viewTechDoc: plugin.techdocs.docRootPage + # createFromTemplate: plugin.scaffolder.templatePage + # - graphiql.page: + # config: + # path: / + # - apis.auth.providers.github: + # config: + # provider: ghe - # - search.page/content: 'app#customSearchPage' # custom search page from somewhere + # - core.signInPage: + # props: + # provider: + # id: google + # title: Google + # message: Sign In using Google + # apiRef: googleAuthApiRef # ??? - # - user-settings.page.routes.advanced: - # at: - # point: user-settings.page/routes - # config: - # title: Advanced - # path: /advanced - # use: '@backstage/plugin-user-settings#AdvancedSettings' + # - core.nav: + # config: + # logo: assets/logo.png + # layout: + # - label: Search + # icon: search + # to: /search + # items: + # - point: search + # - type: divider + # - label: Menu + # icon: menu + # items: + # - label: Home + # icon: home + # to: /catalog + # - label: Create... + # icon: create + # to: /create + # - type: divider + # - type: scroll-wrapper + # items: + # - label: Tech Radar + # icon: map + # to: /tech-radar + # - label: GraphiQL + # icon: graphiql + # to: /graphiql + # - type: divider + # - point: shortcuts + # - type: space + # - type: divider + # - label: Settings + # icon: avatar + # to: /settings + # items: + # - point: settings + # - core.nav/search: '@backstage/plugin-search#SidebarSearchModal' + # - core.nav/shortcuts: + # use: '@backstage/plugin-shortcuts#Shortcuts' + # config: + # allowExternalLinks: true + # - core.nav/settings: '@backstage/plugin-user-settings#SidebarSettings' - # - entity.card.orphanWarning - # - entity.card.processingErrors - # - entity.card.about - # - entity.card.catalogGraph - # - entity.card.pagerDuty - # - entity.card.links - # - entity.card.labels - # - entity.card.githubInsightsLanguages - # - entity.card.githubInsightsReleases - # - entity.card.githubInsightsReadme: - # config: - # maxHeight: 350 # Throwing this config in to have an example, but in practice rely on default - # - entity.card.subcomponentsCard - # - entity.card.userProfile - # - entity.card.ownership - # - entity.card.likeDislikeRatings - # - entity.content.dependsOnComponents - # - entity.content.codeInsights - # - entity.content.todo - # - entity.content.feedbackResponse + # - core.pages.index: + # at: + # point: core.routes + # config: + # path: / + # use: 'react-router-dom#Navigate' + # config: + # to: /catalog + + # - scaffolder.page: + # config: + # groups: + # - title: Recommended + # filter: + # entity.metadata.tags: recommended + # - scaffolder.page/fields: '@backstage/plugin-scaffolder#LowerCaseValuePickerFieldExtension' + # - scaffolder.page/layout: '@backstage/plugin-scaffolder#TwoColumnLayout' + + # - search.page/content: 'app#customSearchPage' # custom search page from somewhere + + # - user-settings.page.routes.advanced: + # at: + # point: user-settings.page/routes + # config: + # title: Advanced + # path: /advanced + # use: '@backstage/plugin-user-settings#AdvancedSettings' + + # - entity.card.orphanWarning + # - entity.card.processingErrors + # - entity.card.about + # - entity.card.catalogGraph + # - entity.card.pagerDuty + # - entity.card.links + # - entity.card.labels + # - entity.card.githubInsightsLanguages + # - entity.card.githubInsightsReleases + # - entity.card.githubInsightsReadme: + # config: + # maxHeight: 350 # Throwing this config in to have an example, but in practice rely on default + # - entity.card.subcomponentsCard + # - entity.card.userProfile + # - entity.card.ownership + # - entity.card.likeDislikeRatings + # - entity.content.dependsOnComponents + # - entity.content.codeInsights + # - entity.content.todo + # - entity.content.feedbackResponse diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index b875fb3927..82b0c927e9 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -17,6 +17,7 @@ import { graphiqlPlugin as legacyGraphiqlPlugin } from '@backstage/plugin-graphiql'; import { createApp as createLegacyApp } from '@backstage/app-defaults'; import { createApp } from '@backstage/frontend-app-api'; +import { pagesPlugin } from './examples/pagesPlugin'; import { graphiqlPlugin } from './examples/graphiqlPlugin'; /* @@ -49,7 +50,7 @@ TODO: /* app.tsx */ const app = createApp({ - plugins: [graphiqlPlugin], + plugins: [graphiqlPlugin, pagesPlugin], // bindRoutes({ bind }) { // bind(catalogPlugin.externalRoutes, { // createComponent: scaffolderPlugin.routes.root, diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx new file mode 100644 index 0000000000..6616c88c4b --- /dev/null +++ b/packages/app-next/src/examples/pagesPlugin.tsx @@ -0,0 +1,100 @@ +/* + * 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 React from 'react'; +import { Link } from '@backstage/core-components'; +import { + createPageExtension, + createPlugin, + useRouteRef, +} from '@backstage/frontend-plugin-api'; +import { createRouteRef } from '@backstage/core-plugin-api'; +import { Route, Routes } from 'react-router-dom'; + +const indexRouteRef = createRouteRef({ id: 'index' }); +const page1RouteRef = createRouteRef({ id: 'page1' }); +// const page2RouteRef = createSubRouteRef({ +// id: 'page2', +// parent: page1RouteRef, +// path: '/page2', +// }); + +const IndexPage = createPageExtension({ + id: 'index', + defaultPath: '/', + routeRef: indexRouteRef, + component: async () => { + const Component = () => { + const page1Link = useRouteRef(page1RouteRef); + return ( +
+ op +
+ Page 1 +
+
+ GraphiQL +
+
+ ); + }; + return ; + }, +}); + +const Page1 = createPageExtension({ + id: 'page1', + defaultPath: '/page1', + routeRef: page1RouteRef, + component: async () => { + const Component = () => { + const indexLink = useRouteRef(indexRouteRef); + // const page2Link = useRouteRef(page2RouteRef); + + return ( +
+

This is page 1

+ Go back + Page 2 + {/* Page 2 */} + +
+ Sub-page content: +
+ + This is also page 1} /> + This is page 2} /> + +
+
+
+ ); + }; + return ; + }, +}); + +export const pagesPlugin = createPlugin({ + id: 'pages', + // routes: { + // index: indexRouteRef, + // // reference in config: + // // 'plugin.pages.routes.index' + // // OR + // // 'page1' + // }, + extensions: [IndexPage, Page1], +}); diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index b1f0097a9d..76a7830707 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -33,6 +33,7 @@ ], "dependencies": { "@backstage/config": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-graphiql": "workspace:^", "@backstage/types": "workspace:^", diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx index e8b429f440..728610679d 100644 --- a/packages/frontend-app-api/src/createApp.tsx +++ b/packages/frontend-app-api/src/createApp.tsx @@ -20,7 +20,7 @@ import { BackstagePlugin, coreExtensionData, } from '@backstage/frontend-plugin-api'; -import { RouteExtension } from './extensions/RouteExtension'; +import { CoreRouter } from './extensions/CoreRouter'; import { createExtensionInstance, ExtensionInstance, @@ -30,6 +30,8 @@ import { mergeExtensionParameters, readAppExtensionParameters, } from './wiring/parameters'; +import { RoutingProvider } from './routing/RoutingContext'; +import { RouteRef } from '@backstage/core-plugin-api'; /** @public */ export function createApp(options: { plugins: BackstagePlugin[] }): { @@ -37,7 +39,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { } { const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any); - const builtinExtensions = [RouteExtension]; + const builtinExtensions = [CoreRouter]; // pull in default extension instance from discovered packages // apply config to adjust default extension instances and add more @@ -83,7 +85,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { return existingInstance; } - const attachments = Object.fromEntries( + const attachments = new Map( Array.from( attachmentMap.get(instanceParams.extension.id)?.entries() ?? [], ).map(([inputName, attachmentConfigs]) => [ @@ -92,12 +94,16 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { ]), ); - return createExtensionInstance({ + const newInstance = createExtensionInstance({ extension: instanceParams.extension, source: instanceParams.source, config: instanceParams.config, attachments, }); + + instances.set(instanceParams.extension.id, newInstance); + + return newInstance; } const rootConfigs = attachmentMap.get('root')?.get('default') ?? []; @@ -105,6 +111,8 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { createInstance(instanceParams), ); + const routePaths = extractRouteInfoFromInstanceTree(rootInstances); + return { createRoot() { const rootComponents = rootInstances.map( @@ -114,12 +122,49 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { ) as typeof coreExtensionData.reactComponent.T, ); return ( - <> + {rootComponents.map((Component, i) => ( ))} - + ); }, }; } + +/** @internal */ +export function extractRouteInfoFromInstanceTree( + roots: ExtensionInstance[], +): Map { + const results = new Map(); + + function visit(current: ExtensionInstance, basePath: string) { + const routePath = current.data.get(coreExtensionData.routePath.id) ?? ''; + const routeRef = current.data.get( + coreExtensionData.routeRef.id, + ) as 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; +} diff --git a/packages/frontend-app-api/src/createExtensionInstance.ts b/packages/frontend-app-api/src/createExtensionInstance.ts index 911955f50a..a7c49d48cb 100644 --- a/packages/frontend-app-api/src/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/createExtensionInstance.ts @@ -19,9 +19,16 @@ import mapValues from 'lodash/mapValues'; /** @internal */ export interface ExtensionInstance { - id: string; - data: Map; - $$type: 'extension-instance'; + readonly id: string; + /** + * Maps extension data ref IDs to extensions produced. + */ + readonly data: Map; + /** + * Maps input names to the actual instances given to them. + */ + readonly attachments: Map; + readonly $$type: 'extension-instance'; } /** @internal */ @@ -29,7 +36,7 @@ export function createExtensionInstance(options: { extension: Extension; config: unknown; source?: BackstagePlugin; - attachments: Record; + attachments: Map; }): ExtensionInstance { const { extension, config, source, attachments } = options; const extensionData = new Map(); @@ -54,7 +61,7 @@ export function createExtensionInstance(options: { extension.inputs, ({ extensionData: pointData }, inputName) => { // TODO: validation - return (attachments[inputName] ?? []).map(attachment => + return (attachments.get(inputName) ?? []).map(attachment => mapValues(pointData, ref => attachment.data.get(ref.id)), ); }, @@ -69,6 +76,7 @@ export function createExtensionInstance(options: { return { id: options.extension.id, data: extensionData, + attachments, $$type: 'extension-instance', }; } diff --git a/packages/frontend-app-api/src/extensions/RouteExtension.tsx b/packages/frontend-app-api/src/extensions/CoreRouter.tsx similarity index 94% rename from packages/frontend-app-api/src/extensions/RouteExtension.tsx rename to packages/frontend-app-api/src/extensions/CoreRouter.tsx index dfa5874d70..f2a760d25e 100644 --- a/packages/frontend-app-api/src/extensions/RouteExtension.tsx +++ b/packages/frontend-app-api/src/extensions/CoreRouter.tsx @@ -21,13 +21,14 @@ import { } from '@backstage/frontend-plugin-api'; import { BrowserRouter, useRoutes } from 'react-router-dom'; -export const RouteExtension = createExtension({ +export const CoreRouter = createExtension({ id: 'core.router', at: 'root', inputs: { routes: { extensionData: { path: coreExtensionData.routePath, + ref: coreExtensionData.routeRef, component: coreExtensionData.reactComponent, }, }, diff --git a/packages/frontend-app-api/src/routing/RoutingContext.tsx b/packages/frontend-app-api/src/routing/RoutingContext.tsx new file mode 100644 index 0000000000..237c8aa5df --- /dev/null +++ b/packages/frontend-app-api/src/routing/RoutingContext.tsx @@ -0,0 +1,52 @@ +/* + * 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 React, { createContext, ReactNode } from 'react'; + +export interface RoutingContextType { + resolve( + routeRef: RouteRef, + options: { pathname: string }, + ): (() => string) | undefined; +} + +export const RoutingContext = createContext({ + resolve: () => () => '', +}); + +export class RouteResolver { + constructor(private readonly routePaths: Map) {} + + resolve(anyRouteRef: RouteRef<{}>): (() => string) | undefined { + const basePath = this.routePaths.get(anyRouteRef); + if (!basePath) { + return undefined; + } + return () => basePath; + } +} + +export function RoutingProvider(props: { + routePaths: Map; + children?: ReactNode; +}) { + return ( + + {props.children} + + ); +} diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 514ca0d82e..a27875bcf8 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -11,6 +11,7 @@ import { ComponentType } from 'react'; import { JsonObject } from '@backstage/types'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; import { z } from 'zod'; import { ZodSchema } from 'zod'; import { ZodTypeDef } from 'zod'; @@ -33,6 +34,7 @@ export const coreExtensionData: { reactComponent: ExtensionDataRef>; routePath: ExtensionDataRef; apiFactory: ExtensionDataRef; + routeRef: ExtensionDataRef>; }; // @public (undocumented) @@ -138,6 +140,7 @@ export function createPageExtension< at?: string; disabled?: boolean; inputs?: TInputs; + routeRef?: RouteRef; component: (props: { config: TConfig; inputs: { @@ -230,4 +233,7 @@ export type PortableSchema = { parse: (input: unknown) => TOutput; schema: JsonObject; }; + +// @public (undocumented) +export function useRouteRef(routeRef: RouteRef): () => string; ``` diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index fa2b4ff093..ba13135b60 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -32,7 +32,8 @@ "dist" ], "peerDependencies": { - "react": "^16.13.1 || ^17.0.0" + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "dependencies": { "@backstage/core-plugin-api": "workspace:^", diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index b3f8942d0d..fb2d930844 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { RouteRef } from '@backstage/core-plugin-api'; import React from 'react'; import { ExtensionBoundary } from '../components'; import { createSchemaFromZod, PortableSchema } from '../createSchemaFromZod'; @@ -46,6 +47,7 @@ export function createPageExtension< at?: string; disabled?: boolean; inputs?: TInputs; + routeRef?: RouteRef; component: (props: { config: TConfig; inputs: { @@ -70,6 +72,7 @@ export function createPageExtension< output: { component: coreExtensionData.reactComponent, path: coreExtensionData.routePath, + ...(options.routeRef && { routeRef: coreExtensionData.routeRef }), }, inputs: options.inputs, configSchema, @@ -87,6 +90,9 @@ export function createPageExtension< )); + if (options.routeRef) { + bind.routeRef!(options.routeRef); + } }, }); } diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 8c87986a14..16f226c483 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -37,3 +37,4 @@ export { type ExtensionDataValue, } from './types'; export * from './wiring'; +export * from './routing'; diff --git a/packages/frontend-plugin-api/src/routing/index.ts b/packages/frontend-plugin-api/src/routing/index.ts new file mode 100644 index 0000000000..4506538fa0 --- /dev/null +++ b/packages/frontend-plugin-api/src/routing/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { useRouteRef } from './useRouteRef'; diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.ts b/packages/frontend-plugin-api/src/routing/useRouteRef.ts new file mode 100644 index 0000000000..ad1c16ea8e --- /dev/null +++ b/packages/frontend-plugin-api/src/routing/useRouteRef.ts @@ -0,0 +1,38 @@ +/* + * 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'; +// eslint-disable-next-line @backstage/no-forbidden-package-imports +import { RoutingContext } from '@backstage/frontend-app-api/src/routing/RoutingContext'; +import { useContext, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; + +/** @public */ +export function useRouteRef(routeRef: RouteRef): () => string { + const { pathname } = useLocation(); + const resolver = useContext(RoutingContext); + + const routeFunc = useMemo( + () => resolver && resolver.resolve(routeRef, { pathname }), + [resolver, routeRef, pathname], + ); + + if (!routeFunc) { + throw new Error(`Failed to resolve routeRef ${routeRef}`); + } + + return routeFunc; +} diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts index 8c59deabfb..940e821af5 100644 --- a/packages/frontend-plugin-api/src/types.ts +++ b/packages/frontend-plugin-api/src/types.ts @@ -15,6 +15,7 @@ */ import { AnyApiFactory } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { PortableSchema } from './createSchemaFromZod'; import { BackstagePlugin } from './wiring'; @@ -37,6 +38,7 @@ export const coreExtensionData = { reactComponent: createExtensionDataRef('core.reactComponent'), routePath: createExtensionDataRef('core.routing.path'), apiFactory: createExtensionDataRef('core.api.factory'), + routeRef: createExtensionDataRef('core.routing.ref'), }; /** @public */ diff --git a/yarn.lock b/yarn.lock index 73d951f571..3d07ca2d41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4316,6 +4316,7 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-graphiql": "workspace:^" "@backstage/types": "workspace:^" @@ -4342,6 +4343,7 @@ __metadata: zod-to-json-schema: ^3.21.4 peerDependencies: react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 languageName: unknown linkType: soft