Token导航 LogoToken导航TokenDH.com
研究检索执行命令unknown未标认证来源可访问许可证需确认审计未展示

plugin-full-frontend-system-migration插件完整前端系统迁移

Agent Skill

用于辅助前端页面、组件、样式和交互逻辑的开发与维护。它适合让 Agent 生成或审查 React、Next.js、Vue、Tailwind、CSS 等相关代码,整理组件结构,或定位布局和性能问题。使用时需要结合项目现有设计系统、路由和构建方式,避免只生成孤立片段;涉及页面改动时,应配合本地预览和构建检查确认视觉效果。

总安装

1,738

周安装

71

下载量

562
Local Agent

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:plugin-full-frontend-system-migration(插件完整前端系统迁移)
来源仓库:https://backstage.io
仓库路径:plugin-full-frontend-system-migration
安装命令:
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。当前暂无明确安装命令,请以来源页面说明为准。

简介

plugin-full-frontend-system-migration 用于辅助前端页面、组件和样式开发,适合 React、Vue 等项目维护场景。

  • 它适用于组件结构整理、布局调整和性能问题定位,可帮助 Agent 生成或审查相关代码片段。
  • 使用时需结合现有设计系统和构建方式,避免生成孤立片段;涉及页面改动时应配合本地预览确认效果。
  • 该技能适用于 Local Agent 环境,无需额外安装命令,但需确保本地开发环境已配置好对应框架。
  • 使用前应检查项目依赖和路由配置,避免破坏现有功能或引入兼容性问题。

SKILL.md

Full Plugin Migration to the New Frontend System

This skill helps fully migrate an existing Backstage plugin from the old frontend system to the new one. Unlike adding dual support (which keeps the old system working), this is a complete migration that removes all @backstage/core-plugin-api usage and makes the plugin work exclusively with the new frontend system.

This is the preferred approach for internal plugins that are only used in a single app, since there is no need to maintain backward compatibility. It can also be used for published plugins when you're ready to drop old system support entirely.

It is highly recommended to be on Backstage version 1.49.x or above before starting this, although not mandatory, you may face issues with some of the instructions below. This can be verified by looking in the backstage.json file in the root of the repository.

Key Differences from Dual Support

AspectDual SupportFull Migration
Entry pointOld src/plugin.ts + new src/alpha.tsxSingle src/plugin.tsx
Plugin creationBoth createPlugin and createFrontendPluginOnly createFrontendPlugin
Core dependencyKeeps @backstage/core-plugin-apiRemoves it, uses only @backstage/frontend-plugin-api
Route refsReuses @backstage/core-plugin-api refs directlyUses createRouteRef from @backstage/frontend-plugin-api
Page shellOld pages keep Page/Header, NFS pages skip itAll pages rely on framework's PageLayout/PluginHeader
Internal routingMay keep legacy <Route> trees in componentsReplaced with SubPageBlueprint tabbed pages
CompatibilityNot neededNot needed

Step 1: Migrate Route Refs

Replace createRouteRef / createSubRouteRef / createExternalRouteRef imports:

// OLD (src/routes.ts)
import {
  createRouteRef,
  createSubRouteRef,
  createExternalRouteRef,
} from '@backstage/core-plugin-api';

export const rootRouteRef = createRouteRef({ id: 'my-plugin' });
export const detailsRouteRef = createSubRouteRef({
  id: 'my-plugin-details',
  parent: rootRouteRef,
  path: '/details/:id',
});
export const externalDocsRouteRef = createExternalRouteRef({ id: 'docs' });

// NEW (src/routes.ts)
import {
  createRouteRef,
  createSubRouteRef,
  createExternalRouteRef,
} from '@backstage/frontend-plugin-api';

export const rootRouteRef = createRouteRef();
export const detailsRouteRef = createSubRouteRef({
  path: '/details/:id',
  parent: rootRouteRef,
});
export const externalDocsRouteRef = createExternalRouteRef({
  defaultTarget: 'techdocs.docRoot',
});

Key differences:

  • createRouteRef() no longer takes an id — the ID is derived from the extension
  • createSubRouteRef path must start with / and must not end with /
  • createExternalRouteRef() no longer takes an id or optional flag

Set Default Targets for External Route Refs

When migrating external route refs, always set defaultTarget to the most common binding target. This removes the need for apps to explicitly bind routes via bindRoutes for standard plugin combinations:

export const createComponentRouteRef = createExternalRouteRef({
  defaultTarget: 'scaffolder.root',
});

export const viewTechDocRouteRef = createExternalRouteRef({
  params: ['namespace', 'kind', 'name'],
  defaultTarget: 'techdocs.docRoot',
});

export const catalogEntityRouteRef = createExternalRouteRef({
  params: ['namespace', 'kind', 'name'],
  defaultTarget: 'catalog.catalogEntity',
});

The defaultTarget string uses the <pluginId>.<routeName> format, where routeName matches a key in the target plugin's routes map. The default is only activated when the target plugin is installed — otherwise the route stays unbound and useRouteRef returns undefined.

This is especially important for a full migration because in the old system, apps typically had explicit bindRoutes calls. With default targets, most of those bindings become unnecessary, improving the plug-and-play experience.

Step 2: Migrate the Plugin Definition

Replace src/plugin.ts with a createFrontendPlugin-based definition:

// NEW (src/plugin.tsx)
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
import { RiToolsLine } from '@remixicon/react';
import { rootRouteRef, externalDocsRouteRef } from './routes';
import { myPage } from './extensions';
import { myPluginApi } from './apis';

export default createFrontendPlugin({
  pluginId: 'my-plugin',
  title: 'My Plugin',
  icon: <RiToolsLine />,
  info: {
    packageJson: () => import('../package.json'),
  },
  routes: {
    root: rootRouteRef,
  },
  externalRoutes: {
    docs: externalDocsRouteRef,
  },
  extensions: [myPluginApi, myPage],
});

For the plugin icon, prefer using Remix Icons from @remixicon/react. If the plugin already has an existing MUI icon, it can be kept with fontSize="inherit" (e.g. <CategoryIcon fontSize="inherit" />), but for new icons Remix is the recommended choice.

Since this is the only entry point now, export it as default from src/index.ts or update package.json exports accordingly. If the plugin was previously consumed via its main entry point, you can make the main entry point export the new plugin:

{
  "exports": {
    ".": "./src/index.ts",
    "./package.json": "./package.json"
  }
}
// src/index.ts
export { default } from './plugin';
export { rootRouteRef } from './routes';

Step 3: Migrate API Factories to ApiBlueprint

// OLD
import {
  createApiFactory,
  discoveryApiRef,
  fetchApiRef,
} from '@backstage/core-plugin-api';

export const myApiFactory = createApiFactory({
  api: myPluginApiRef,
  deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
  factory: ({ discoveryApi, fetchApi }) =>
    new MyPluginClient({ discoveryApi, fetchApi }),
});

// NEW (src/apis.ts)
import {
  ApiBlueprint,
  discoveryApiRef,
  fetchApiRef,
} from '@backstage/frontend-plugin-api';
import { myPluginApiRef } from './api';

export const myPluginApi = ApiBlueprint.make({
  params: defineParams =>
    defineParams({
      api: myPluginApiRef,
      deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
      factory: ({ discoveryApi, fetchApi }) =>
        new MyPluginClient({ discoveryApi, fetchApi }),
    }),
});

Also update the API ref creation to the new builder pattern with explicit pluginId:

// OLD
import { createApiRef } from '@backstage/core-plugin-api';

export const myPluginApiRef = createApiRef<MyPluginApi>({
  id: 'plugin.my-plugin.client',
});

// NEW (recommended builder pattern with explicit pluginId)
import { createApiRef } from '@backstage/frontend-plugin-api';

export const myPluginApiRef = createApiRef<MyPluginApi>().with({
  id: 'plugin.my-plugin.client',
  pluginId: 'my-plugin',
});

The builder form (createApiRef<T>().with(...)) is preferred because ownership is explicit via pluginId rather than parsed from the ID string. The id must still be globally unique across the app — the pluginId is ownership metadata, not a namespace prefix.

API Ownership and Override Rules

The new system enforces API ownership — only the owning plugin (or a module targeting it) can provide or override a given API. Ownership is determined by:

  1. The explicit pluginId on the ApiRef (if set via the builder pattern)
  2. Falling back to inference from the ApiRef ID string:

- plugin.<pluginId>.* → owned by that plugin - core.* → owned by the app plugin

If app adopters want to replace your plugin's default API implementation, they must use a createFrontendModule with pluginId matching your plugin — they cannot override it from a different plugin or from a generic app module. This is a stricter model than the old system where any API could be overridden from the app's apis array.

Step 4: Migrate Pages to PageBlueprint

Simple Page (No Sub-Routes)

// src/extensions.tsx
import { PageBlueprint } from '@backstage/frontend-plugin-api';
import { rootRouteRef } from './routes';

export const myPage = PageBlueprint.make({
  params: {
    path: '/my-plugin',
    routeRef: rootRouteRef,
    loader: () => import('./components/MyPage').then(m => <m.MyPage />),
  },
});

The MyPage component should not include Page, Header, or PageWithHeader from @backstage/core-components. The framework's PageLayout renders PluginHeader automatically.

The title and icon params on PageBlueprint are only needed if they should differ from the plugin's own title and icon (set in createFrontendPlugin). If omitted, the plugin-level values are used.

Page with Header for Custom Actions

If your page needs a subtitle or action buttons below the framework header, use Header from @backstage/ui:

// src/components/MyPage/MyPage.tsx
import { Header } from '@backstage/ui';
import { Content } from '@backstage/core-components';

export function MyPage() {
  return (
    <>
      <Header
        title="Subtitle or description"
        customActions={
          <>
            <CreateButton title="Create" to="/my-plugin/create" />
            <SupportButton>Help text</SupportButton>
          </>
        }
      />
      <Content>
        <MyPageContent />
      </Content>
    </>
  );
}

Page Without Header

For pages that manage their own layout entirely (e.g. home page, dashboards), set noHeader: true:

export const myPage = PageBlueprint.make({
  params: {
    path: '/my-plugin',
    routeRef: rootRouteRef,
    noHeader: true,
    loader: () => import('./components/MyPage').then(m => <m.MyPage />),
  },
});

Step 5: Replace Internal Routing with Sub-Pages

Old frontend plugins often use React Router <Route> trees inside a router component to handle internal navigation. Before migrating, determine which routing pattern fits the plugin.

Decide Which Routing Pattern to Use

Not all internal routing maps to tabs. Read the plugin's existing router component and ask the user:

"Does your plugin use top-level tabs that users navigate between via a header (e.g. Overview / Settings)? Or does it use detail/drill-down routes (e.g. /my-plugin/items/:id)?"

Use SubPageBlueprint when:

  • The sub-routes represent top-level tabs/sections of the plugin
  • Users navigate between them via the header

Keep internal routing within a PageBlueprint loader when:

  • Routes are detail/drill-down pages (e.g. /my-plugin/items/:id)
  • The routing is deeply nested or dynamic

If the plugin uses drill-down routing only, use a PageBlueprint with a loader that handles its own <Routes> and skip the rest of this step:

export const myPage = PageBlueprint.make({
  params: {
    path: '/my-plugin',
    routeRef: rootRouteRef,
    loader: () => import('./components/Router').then(m => <m.MyPluginRouter />),
  },
});

If the plugin uses top-level tabs, continue with the SubPageBlueprint migration below.

Old Pattern: Internal Router

// OLD — plugin owns its own routing
import { Route, Routes } from 'react-router-dom';

export function MyPluginRouter() {
  return (
    <Page themeId="tool">
      <Header title="My Plugin" />
      <HeaderTabs
        tabs={[
          { id: 'overview', label: 'Overview' },
          { id: 'settings', label: 'Settings' },
        ]}
      />
      <Content>
        <Routes>
          <Route path="/" element={<OverviewPage />} />
          <Route path="/settings" element={<SettingsPage />} />
        </Routes>
      </Content>
    </Page>
  );
}

New Pattern: PageBlueprint + SubPageBlueprint

// src/extensions.tsx
import {
  PageBlueprint,
  SubPageBlueprint,
} from '@backstage/frontend-plugin-api';

// Parent page WITHOUT a loader — uses built-in tabbed rendering
export const myPluginPage = PageBlueprint.make({
  params: {
    path: '/my-plugin',
    routeRef: rootRouteRef,
  },
});

export const overviewSubPage = SubPageBlueprint.make({
  name: 'overview',
  params: {
    path: 'overview',
    title: 'Overview',
    loader: () =>
      import('./components/OverviewPage').then(m => <m.OverviewPageContent />),
  },
});

export const settingsSubPage = SubPageBlueprint.make({
  name: 'settings',
  params: {
    path: 'settings',
    title: 'Settings',
    loader: () =>
      import('./components/SettingsPage').then(m => <m.SettingsPageContent />),
  },
});

How this works:

  • PageBlueprint without a loader automatically renders its sub-pages as tabs
  • The first sub-page becomes the default (index redirect)
  • Each SubPageBlueprint gets a tab in the header with its title
  • Sub-page path values are relative (no leading /)
  • Sub-page components render content only — no Page, Header, or HeaderTabs

If the sub-page content needs padding, use Container from @backstage/ui as a wrapper inside the component.

Step 6: Update Hooks and Imports

Replace all @backstage/core-plugin-api imports with @backstage/frontend-plugin-api:

// OLD
import { useApi, useRouteRef, configApiRef } from '@backstage/core-plugin-api';

// NEW
import {
  useApi,
  useRouteRef,
  configApiRef,
} from '@backstage/frontend-plugin-api';

useRouteRef Behavior Change

In the new system, useRouteRef may return undefined for external route refs that aren't bound. Handle this:

// OLD — throws if not bound
const docsLink = useRouteRef(externalDocsRouteRef);
// Always a function

// NEW — returns undefined if not bound
const docsLink = useRouteRef(externalDocsRouteRef);
if (docsLink) {
  // render link
}

Common Import Mappings

Old Import (@backstage/core-plugin-api)New Import (@backstage/frontend-plugin-api)
createPlugincreateFrontendPlugin
createRouteRefcreateRouteRef
createSubRouteRefcreateSubRouteRef
createExternalRouteRefcreateExternalRouteRef
createApiRefcreateApiRef
createApiFactoryApiBlueprint.make
useApiuseApi
useRouteRefuseRouteRef
configApiRefconfigApiRef
discoveryApiRefdiscoveryApiRef
fetchApiReffetchApiRef
identityApiRefidentityApiRef
storageApiRefstorageApiRef
analyticsApiRefanalyticsApiRef
createRoutableExtensionPageBlueprint.make
createComponentExtensionDepends on context — blueprint or createExtension

Step 7: Remove Old System Code

  1. Delete src/plugin.ts (old createPlugin)
  2. Delete any createRoutableExtension / createComponentExtension usage
  3. Remove Page, Header, PageWithHeader wrapping from page components
  4. Remove HeaderTabs if replaced by SubPageBlueprint tabs
  5. Remove internal <Routes>/<Route> trees if replaced by sub-pages
  6. Remove @backstage/core-plugin-api from package.json dependencies
  7. Remove @backstage/core-compat-api from package.json dependencies if present

Step 8: Update Page Components for BUI

With the full migration, page components should use @backstage/ui components and patterns. See the mui-to-bui-migration skill for detailed component migration guidance.

Key page-level changes:

  • Replace PageWithHeader / Page + Header with framework-provided PluginHeader (automatic via PageLayout)
  • Use Header from @backstage/ui for optional subtitle/custom actions
  • Use Content from @backstage/core-components for page body padding (this is still used even in NFS pages)
  • Replace ContentHeader with Header's customActions prop
  • Replace HeaderTabs with SubPageBlueprint (tabs are rendered by the framework)

Real Example: Auth Plugin (Fully Migrated)

The @backstage/plugin-auth plugin is a fully migrated example with no @backstage/core-plugin-api dependency:

// plugins/auth/src/routes.ts
import { createRouteRef } from '@backstage/frontend-plugin-api';

export const rootRouteRef = createRouteRef();

// plugins/auth/src/plugin.tsx
import {
  createFrontendPlugin,
  PageBlueprint,
} from '@backstage/frontend-plugin-api';
import { rootRouteRef } from './routes';

export const AuthPage = PageBlueprint.make({
  params: {
    path: '/oauth2',
    routeRef: rootRouteRef,
    loader: () => import('./components/Router').then(m => <m.Router />),
  },
});

export default createFrontendPlugin({
  pluginId: 'auth',
  extensions: [AuthPage],
  routes: {
    root: rootRouteRef,
  },
});

Real Example: Scaffolder Sub-Pages

The scaffolder plugin demonstrates the sub-page pattern (though it still has dual support — the pattern itself is what a full migration targets):

// PageBlueprint WITHOUT loader — framework renders tabs
export const scaffolderPage = PageBlueprint.make({
  params: {
    path: '/create',
    routeRef: rootRouteRef,
  },
});

// Sub-pages with content only
export const templatesSubPage = SubPageBlueprint.make({
  name: 'templates',
  params: {
    path: 'templates',
    title: 'Templates',
    loader: () => import('./TemplatesPage').then(m => <m.TemplatesSubPage />),
  },
});

export const tasksSubPage = SubPageBlueprint.make({
  name: 'tasks',
  params: {
    path: 'tasks',
    title: 'Tasks',
    loader: () => import('./TasksPage').then(m => <m.TasksSubPage />),
  },
});

Migration Checklist

  1. Migrate route refs to @backstage/frontend-plugin-api (createRouteRef, createSubRouteRef, createExternalRouteRef)
  2. Replace createPlugin with createFrontendPlugin
  3. Convert all API factories to ApiBlueprint extensions
  4. Convert pages to PageBlueprint
  5. Replace internal tab routing with SubPageBlueprint where appropriate
  6. Remove Page/Header/PageWithHeader from page components
  7. Add Header from @backstage/ui where subtitle/custom actions are needed
  8. Replace HeaderTabs with SubPageBlueprint tabs
  9. Update all @backstage/core-plugin-api imports to @backstage/frontend-plugin-api
  10. Handle useRouteRef possibly returning undefined
  11. Remove src/plugin.ts (old system entry point)
  12. Remove src/alpha.tsx if it existed (merge into main entry)
  13. Remove @backstage/core-plugin-api from package.json dependencies
  14. Remove @backstage/core-compat-api from package.json dependencies
  15. Update package.json exports (remove ./alpha if merged into main)
  16. Run yarn tsc to check for type errors
  17. Run yarn lint to check for missing dependencies
  18. Run yarn build:api-reports to update API reports (if the project uses API reports)
  19. Test in a new-system app (packages/app)

Reference

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Local Agent

77.09%
按下载量换算433

安全审计

暂无安全审计结果可展示。

权限和风险

执行命令

安装流程涉及命令执行,可能通过 第三方 CLI 联网下载 Skill 或依赖。用户安装前应确认命令来源、仓库内容和执行环境。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills