Migrating Plugins
This guide allows you to migrate a frontend plugin and its own components, routes, apis to the new frontend system.
The main concept is that routes, components, apis are now extensions. You can use the appropriate extension blueprints to migrate all of them to extensions.
Migrating the plugin
Unless you are migrating a plugin that is only used within your own project, we recommend all plugins to keep support for the old system intact. The code added in these examples should be added to a new src/alpha.tsx
entry point of your plugin.
In the legacy frontend system a plugin was defined in its own plugin.ts
file as following:
import { createPlugin } from '@backstage/core-plugin-api';
export const myPlugin = createPlugin({
id: 'my-plugin',
apis: [ ... ],
routes: {
...
},
externalRoutes: {
...
},
});
In order to migrate the actual definition of the plugin you need to recreate the plugin using the new createFrontendPlugin
utility exported by @backstage/frontend-plugin-api
. The new createFrontendPlugin
function doesn't accept apis anymore as apis are now extensions.
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
import { convertLegacyRouteRefs } from '@backstage/core-compat-api';
export default createFrontendPlugin({
id: 'my-plugin',
// bind all the extensions to the plugin
extensions: [/* APIs will go here, but don't worry about those yet */],
// convert old route refs to the new system
routes: convertLegacyRouteRefs({
...
}),
externalRoutes: convertLegacyRouteRefs({
...
}),
});
The code above binds all the extensions to the plugin. Important: Make sure to export the plugin as default export of your package as a separate entrypoint, preferably /alpha
, as suggested by the code snippet above. Make sure src/alpha.tsx
is exported in your package.json
:
"exports": {
".": "./src/index.ts",
"./alpha": "./src/alpha.tsx",
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"alpha": [
"src/alpha.tsx"
],
"package.json": [
"package.json"
]
}
},
Migrating Pages
Pages that were previously created using the createRoutableExtension
extension function can be migrated to the new Frontend System using the PageBlueprint
extension blueprint, exported by @backstage/frontend-plugin-api
.
In the new system plugins provide more information than they used to. For example, the plugin is now responsible for providing the path for the page, rather than it being part of the app code.
For example, given the following page:
export const FooPage = fooPlugin.provide(
createRoutableExtension({
name: 'FooPage',
component: () => import('./components').then(m => m.FooPage),
mountPoint: rootRouteRef,
}),
);
and the following instruction in the plugin README:
<Route path="/foo" element={<FooPage />} />
it can be migrated as the following, keeping in mind that you may need to switch from .ts
to .tsx
:
import { PageBlueprint } from '@backstage/frontend-plugin-api';
import {
compatWrapper,
convertLegacyRouteRef,
} from '@backstage/core-compat-api';
const fooPage = PageBlueprint.make({
params: {
// This is the path that was previously defined in the app code.
// It's labelled as the default one because it can be changed via configuration.
defaultPath: '/foo',
// You can reuse the existing routeRef by wrapping it with convertLegacyRouteRef.
routeRef: convertLegacyRouteRef(rootRouteRef),
// these inputs usually match the props required by the component.
loader: () =>
import('./components/').then(m =>
// The compatWrapper utility allows you to keep using @backstage/core-plugin-api in the
// implementation of the component and switch to @backstage/frontend-plugin-api later.
compatWrapper(<m.FooPage />),
),
},
});
Then add the fooPage
extension to the plugin:
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
export default createFrontendPlugin({
id: 'my-plugin',
// bind all the extensions to the plugin
extensions: [],
extensions: [fooPage],
...
});
Migrating Components
The equivalent utility to replace components created with createComponentExtension
depends on the context within which the component is used, typically indicated by the naming pattern of the export. Many of these can be migrated to one of the existing blueprints, but in rare cases it may be necessary to use createExtension
directly.
Migrating APIs
There are a few things to keep in mind in regards to utility APIs.
React package interface and ref changes
Let's begin with your -react
package. The act of exporting TypeScript interfaces and API refs have not changed from the old system. You can typically keep those as-is. For illustrative purposes, this is an example of an interface and its API ref:
import { createApiRef } from '@backstage/frontend-plugin-api';
/**
* Performs some work.
* @public
*/
export interface WorkApi {
doWork(): Promise<void>;
}
/**
* The work interface for the Example plugin.
* @public
*/
export const workApiRef = createApiRef<WorkApi>({
id: 'plugin.example.work',
});
Note at the top of the file that it uses the updated import from @backstage/frontend-plugin-api
that we migrated in the previous section, instead of the old @backstage/core-plugin-api
.
Now let's migrate the implementation of the api. Before we changed the core-plugin-api
imports the api would have looked somewhat similar to the following:
import { storageApiRef, createApiFactory } from '@backstage/core-plugin-api';
import { workApiRef } from '@internal/plugin-example-react';
import { WorkImpl } from './WorkImpl';
const exampleWorkApi = createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => new WorkImpl({ storageApi }),
});
The major changes we'll make are
- Change the old
@backstage/core-plugin-api
imports to the new@backstage/frontend-plugin-api
package as per the top section of this guide - Wrap the existing API factory in a
ApiBlueprint
The end result, after simplifying imports and cleaning up a bit, might look like this:
import {
storageApiRef,
createApiFactory,
ApiBlueprint,
} from '@backstage/frontend-plugin-api';
import { workApiRef } from '@internal/plugin-example-react';
import { WorkImpl } from './WorkImpl';
const exampleWorkApi = ApiBlueprint.make({
params: {
factory: createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => new WorkImpl({ storageApi }),
}),
},
});
Finally, let's add the exampleWorkApi
extension to the plugin:
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
export default createFrontendPlugin({
id: 'my-plugin',
// bind all the extensions to the plugin
extensions: [fooPage],
extensions: [exampleWorkApi, fooPage],
...
});
Further work
Since utility APIs are now complete extensions, you may want to take a bigger look at how they used to be used, and what the new frontend system offers. You may for example consider adding configurability or inputs to your API, if that makes sense for your current application.