From 5f17e70db6f1da0505f45c8cde731e242d6884f5 Mon Sep 17 00:00:00 2001 From: Rohit Rai Date: Thu, 25 Jun 2026 19:11:48 +0530 Subject: [PATCH] fix: address PR #45 review feedback - Bump version to 0.6.0 (rebase on main which is at 0.5.0) - Remove @ prefix from backstage-upgrade in routing table - Add dependency note for cross-skill version reference - Add cross-references between NFS guide and skill references Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 6 +- docs/nfs-migration-guide.md | 484 ------------------ skills/nfs-migration/SKILL.md | 34 +- .../nfs-migration/references/api-changes.md | 2 +- skills/nfs-migration/references/app-setup.md | 10 +- skills/nfs-migration/references/gotchas.md | 82 ++- .../references/migrate-app-level.md | 15 + .../references/migrate-entity-content.md | 104 ++++ .../references/migrate-rhdh-extensions.md | 151 ++++++ .../references/mount-point-mapping.md | 114 ++++- .../references/operator-config.md | 123 +++++ .../nfs-migration/references/package-json.md | 89 +++- skills/nfs-migration/references/support.md | 9 + .../nfs-migration/references/verification.md | 24 +- 14 files changed, 695 insertions(+), 552 deletions(-) delete mode 100644 docs/nfs-migration-guide.md create mode 100644 skills/nfs-migration/references/operator-config.md diff --git a/README.md b/README.md index 755de14..39d5b17 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,7 @@ Build dynamic plugins from scratch — backend or frontend — and get them depl Migrate your plugins from the legacy Backstage frontend system to the New Frontend System (NFS). -Start with the **[NFS Migration Guide](./docs/nfs-migration-guide.md)** -- it covers what NFS is, why you need to migrate, the deprecation timeline, and walks through every migration pattern with code examples. - -When you're ready to migrate, use the Agent Skill to automate it: - -- **[nfs-migration](./skills/nfs-migration/SKILL.md)** -- Analyzes your existing plugin, applies the right Blueprint patterns, updates exports, and verifies the result. Two approaches: direct-to-GA (recommended) or phased with backward compatibility. +- **[nfs-migration](./skills/nfs-migration/SKILL.md)** -- Analyzes your existing plugin, applies the right Blueprint patterns, updates exports, and verifies the result. Two approaches: direct-to-GA (recommended) or phased with backward compatibility. Reference files cover every extension type, mount point mapping, operator config, gotchas, and verification. ### Backstage Upgrade diff --git a/docs/nfs-migration-guide.md b/docs/nfs-migration-guide.md deleted file mode 100644 index a519cd7..0000000 --- a/docs/nfs-migration-guide.md +++ /dev/null @@ -1,484 +0,0 @@ -# Migrating RHDH Plugins to the New Frontend System (NFS) - -A practical guide for Red Hat Developer Hub plugin authors migrating from the legacy Backstage frontend system to NFS. - -> **Agent skill users:** The `nfs-migration` skill (`skills/nfs-migration/`) contains the same patterns broken into reference files optimized for agent consumption. This guide is the authoritative human-readable source. When updating migration patterns, update this guide first, then sync the corresponding reference file. - ---- - -## 1. What is the New Frontend System - -The Backstage New Frontend System (NFS) replaces the legacy frontend plugin API. Instead of manually wiring plugins into an app with `createPlugin`, `createRoutableExtension`, `FlatRoutes`, and imperative JSX route trees, NFS uses declarative extension **Blueprints** (`PageBlueprint`, `ApiBlueprint`, `EntityContentBlueprint`, etc.) and `createFrontendPlugin` from `@backstage/frontend-plugin-api`. - -The app assembles itself from features: - -```ts -import { createApp } from '@backstage/frontend-defaults'; - -const app = createApp({ features: [myPlugin, catalogPlugin, ...] }); -``` - -Plugins declare what they provide. The app decides what to render. - ---- - -## 2. Why Migrate - -- **Declarative**: plugins describe their own routes, nav items, and APIs -- no more manual wiring in the app -- **Configurable**: extensions can be enabled, disabled, or reordered via `app-config.yaml` -- **Auto-discoverable**: apps can detect installed plugins automatically -- **Composable**: modules can inject extensions into other plugins (e.g. entity tabs into catalog) -- **Required**: the legacy APIs are being deprecated and will be removed - ---- - -## 3. Deprecation Timeline - -| Phase | What happens | -|-------|-------------| -| **Current (RHDH 1.10)** | NFS available as `/alpha` exports alongside legacy | -| **Next release (GA)** | NFS becomes the root export (`.`); legacy moves to `/legacy` with `@deprecated` tags | -| **GA + 2 releases** | Legacy `/legacy` exports removed entirely | - ---- - -## 4. Key Concepts - -### Blueprints vs Legacy Extension Factories - -Blueprints are declarative factories that replace imperative helpers like `createRoutableExtension()`. Each blueprint type (`PageBlueprint`, `ApiBlueprint`, `EntityContentBlueprint`) knows how to register itself with the app. Nav items are auto-discovered from pages -- no separate blueprint needed. - -```ts -// Legacy -export const MyPage = myPlugin.provide(createRoutableExtension({ ... })); - -// NFS -const myPage = PageBlueprint.make({ params: { path: '/my-plugin', loader: () => ... } }); -``` - -### `createFrontendPlugin` vs `createPlugin` - -| Legacy `createPlugin` | NFS `createFrontendPlugin` | -|---|---| -| `id: 'my-plugin'` | `pluginId: 'my-plugin'` | -| `apis: [createApiFactory(...)]` | APIs go in `extensions` array as `ApiBlueprint` | -| Pages/routes wired externally | Pages declared as `PageBlueprint` in `extensions` | -| Named export | **Default export** | - -### `createFrontendModule` - -Bundles extensions that target *another* plugin. Common cases: - -- **Translations** target `pluginId: 'app'` -- **Homepage widgets** target `pluginId: 'home'` - -Modules are separate exports, not part of the plugin itself. Note: entity content and cards can go directly in your plugin's `extensions` array (they declare their own attach point) — a separate catalog module is only needed when injecting content from outside a plugin you don't own. - -### Route Refs - -You can reuse existing route refs from `@backstage/core-plugin-api` or create new ones from `@backstage/frontend-plugin-api`. Both work -- no need to migrate route refs immediately. - ---- - -## 5. Migration Patterns - -### Plugin Definition - -**Legacy:** - -```ts -import { createPlugin, createApiFactory, configApiRef, fetchApiRef } from '@backstage/core-plugin-api'; -import { rootRouteRef } from './routes'; -import { myApiRef, MyApiClient } from './api'; - -export const myPlugin = createPlugin({ - id: 'my-plugin', - routes: { root: rootRouteRef }, - apis: [ - createApiFactory({ - api: myApiRef, - deps: { configApi: configApiRef, fetchApi: fetchApiRef }, - factory: ({ configApi, fetchApi }) => new MyApiClient({ configApi, fetchApi }), - }), - ], -}); -``` - -**NFS:** - -```tsx -import { - createFrontendPlugin, ApiBlueprint, PageBlueprint, - configApiRef, fetchApiRef, createApiFactory, -} from '@backstage/frontend-plugin-api'; -import { rootRouteRef } from './routes'; -import { myApiRef, MyApiClient } from './api'; -import { RiToolsLine } from '@remixicon/react'; - -const myApi = ApiBlueprint.make({ - params: defineParams => defineParams({ - api: myApiRef, - deps: { configApi: configApiRef, fetchApi: fetchApiRef }, - factory: ({ configApi, fetchApi }) => new MyApiClient({ configApi, fetchApi }), - }), -}); - -const myPage = PageBlueprint.make({ - params: { - path: '/my-plugin', - title: 'My Plugin', - icon: , - routeRef: rootRouteRef, - loader: () => import('./components/MyPage').then(m => ), - }, -}); - -export default createFrontendPlugin({ - pluginId: 'my-plugin', - title: 'My Plugin', - icon: , - extensions: [myApi, myPage], - routes: { root: rootRouteRef }, -}); -``` - -Key changes: APIs and pages are extensions in the `extensions` array. Nav items are auto-discovered from pages with `title`, `icon`, and `routeRef`. The plugin is the **default export**. - -### Pages - -**Legacy** -- routable extension provided by the plugin, path set in the app's `FlatRoutes`: - -```tsx -export const MyPage = myPlugin.provide( - createRoutableExtension({ - name: 'MyPage', - component: () => import('./components/MyPage').then(m => m.MyPage), - mountPoint: rootRouteRef, - }), -); - -// In the app: - - } /> - -``` - -**NFS** -- the plugin owns its path. No app-side route wiring. The NFS page component must **not** include its own page shell (`PageWithHeader`) — the framework provides the header automatically: - -```tsx -const myPage = PageBlueprint.make({ - params: { - path: '/my-plugin', - routeRef: rootRouteRef, - loader: () => import('./components/MyPage').then(m => ), - }, -}); -``` - -Create a separate NFS variant of each page component without the page shell. See `references/migrate-page.md` for the dual header pattern (Pattern A for simple pages, Pattern B for complex pages). - -### Nav Items - -**Legacy** -- manually added in the app's sidebar: - -```tsx - -``` - -**NFS** -- auto-discovered from pages. Set `title` and `icon` on `PageBlueprint` params and the app generates nav entries automatically. No separate blueprint needed — see the Plugin Definition example above. - -> Earlier Backstage versions used `NavItemBlueprint`. It has been removed — see `references/api-changes.md`. - -### APIs - -**Legacy** -- `createApiFactory` in the plugin's `apis` array. - -**NFS** -- wrap the existing `createApiFactory` call in `ApiBlueprint.make` using the `defineParams` callback. See the Plugin Definition example above. The `defineParams` callback is required -- it's how the blueprint validates the factory. See `references/migrate-page.md` for the full pattern. - -### Entity Content (Catalog Tabs) - -Entity content goes in your plugin's `extensions` array. The blueprint declares its own attach point, so the app discovers it automatically: - -```tsx -import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; -import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; - -const myEntityContent = EntityContentBlueprint.make({ - params: { - path: '/my-tab', - title: 'My Tab', - loader: () => import('./components/MyTab').then(m => ), - }, -}); - -export default createFrontendPlugin({ - pluginId: 'my-plugin', - extensions: [myEntityContent], -}); -``` - -If you need to provide entity content from a separate package (third-party addon), use `createFrontendModule({ pluginId: 'catalog' })` instead. - -### Translations - -Translations must be in a separate module targeting `pluginId: 'app'`: - -```tsx -import { createFrontendModule } from '@backstage/frontend-plugin-api'; -import { TranslationBlueprint } from '@backstage/plugin-app-react'; -import { myTranslations } from './translations'; - -export const myTranslationsModule = createFrontendModule({ - pluginId: 'app', - extensions: [ - TranslationBlueprint.make({ - name: 'my-plugin-translations', - params: { resource: myTranslations }, - }), - ], -}); -``` - -### RHDH-Specific Extensions - -**Drawer panels** -- `AppDrawerContentBlueprint`: - -```tsx -import { AppDrawerContentBlueprint } from '@red-hat-developer-hub/backstage-plugin-app-react/alpha'; - -const myDrawer = AppDrawerContentBlueprint.make({ - params: { - title: 'My Drawer', - loader: () => import('./components/MyDrawer').then(m => ), - }, -}); -``` - -**Global header menu items** -- `GlobalHeaderMenuItemBlueprint`: - -```tsx -import { GlobalHeaderMenuItemBlueprint } from '@red-hat-developer-hub/backstage-plugin-global-header/alpha'; - -const myMenuItem = GlobalHeaderMenuItemBlueprint.make({ - params: { - title: 'My Action', - icon: MyIcon, - routeRef: rootRouteRef, - }, -}); -``` - -**Homepage widgets** -- `HomePageWidgetBlueprint`: - -```tsx -import { HomePageWidgetBlueprint } from '@backstage/plugin-home-react/alpha'; - -const myWidget = HomePageWidgetBlueprint.make({ - params: { - title: 'My Widget', - loader: () => import('./components/MyWidget').then(m => ), - }, -}); -``` - -### RHDH Mount Point Migration - -If your plugin uses RHDH dynamic plugin mount points (`app-config.dynamic.yaml`), these map directly to NFS blueprints. See `references/mount-point-mapping.md` for the complete mapping table with before/after examples for each mount point type. - -### Shared Components (Legacy + NFS) - -Keep component imports (`useApi`, `useRouteRef`, etc.) on `@backstage/core-plugin-api` — they work in both legacy and NFS contexts. This lets the same components serve both export paths without changes: - -```tsx -// Keep this — works in both legacy and NFS -import { useApi, useRouteRef } from '@backstage/core-plugin-api'; -``` - -Don't migrate component imports to `@backstage/frontend-plugin-api` if you need to support legacy consumers — it breaks the legacy code path. - -### CompatWrapper (rare) - -Only needed when a component depends on legacy context providers that aren't available in NFS (e.g. old `SidebarContext`). Most plugins won't need this. Wrap the JSX element in the loader: - -```tsx -loader: () => import('./components/MyPage').then(m => compatWrapper()) -``` - -Import `compatWrapper` from `@backstage/core-compat-api`. - ---- - -## 6. Choosing Your Approach - -### Approach A -- Direct to GA (recommended) - -NFS becomes the root export immediately. Legacy code moves to `/legacy` or is removed. - -Best when: -- You control all consumers -- You can do a clean migration in one pass -- You want the simplest result - -### Approach B -- Phased - -Add NFS as `/alpha` exports alongside existing legacy exports. Graduate later by swapping. - -Best when: -- External consumers depend on legacy exports -- You need time to migrate tests and stories -- You want to ship incrementally - -| | Direct to GA | Phased | -|---|---|---| -| Complexity | Lower | Higher (two export sets) | -| Consumer impact | Breaking change | Non-breaking initially | -| Maintenance | One code path | Two code paths temporarily | -| Recommended for | Internal plugins | Shared/published plugins | - ---- - -## 7. Package.json Changes - -Update your `package.json` exports for the GA structure: - -```json -{ - "exports": { - ".": "./src/index.ts", - "./legacy": "./src/legacy.ts", - "./package.json": "./package.json" - }, - "typesVersions": { - "*": { - "legacy": ["src/legacy.ts"], - "package.json": ["package.json"] - } - } -} -``` - -- `.` -- NFS plugin (default export from `createFrontendPlugin`) -- `./legacy` -- old `createPlugin`-based exports for consumers who haven't migrated yet -- Remove the `./legacy` entry when you drop legacy support - ---- - -## 8. Verifying Your Migration - -Run through this checklist: - -- [ ] `yarn tsc` passes with no type errors -- [ ] `yarn build` succeeds -- [ ] Plugin default export is the `createFrontendPlugin` result -- [ ] All extensions (pages, APIs) are in the `extensions` array -- [ ] NFS page components don't include `PageWithHeader`/`Page` shell (dual header pattern) -- [ ] Routes are declared in the plugin's `routes` object -- [ ] Translations are in a separate `createFrontendModule` with `pluginId: 'app'` -- [ ] Entity content extensions are in the plugin's `extensions` array -- [ ] `package.json` exports are updated (`.` for NFS, `./legacy` for old) -- [ ] `src/index.ts` does NOT re-export legacy APIs (legacy only via `./legacy` subpath) -- [ ] Plugin file uses `.tsx` extension if it contains JSX in blueprint loaders -- [ ] Component imports stay on `@backstage/core-plugin-api` (shared between legacy and NFS) -- [ ] `dev/index.tsx` uses NFS dev app pattern; legacy dev app moved to `dev/legacy.tsx` -- [ ] Workspace app (`packages/app`) is NFS; legacy consumers moved to `./legacy` subpath or a separate `packages/app-legacy` - ---- - -## 9. Testing with RHDH - -### Local Testing - -Use the `rhdh-local` skill to test in a local RHDH instance. If NFS is not yet the default app shell, enable it with environment variables: - -```bash -APP_CONFIG_app_packageName=app-next -ENABLE_STANDARD_MODULE_FEDERATION=true -``` - -Export the plugin as a dynamic plugin and deploy it locally. Verify that: -- The plugin loads without errors -- Nav items appear in the sidebar -- Pages render at the correct paths -- Entity tabs show up on the right entity kinds - -### Cluster Testing - -For OpenShift/K8s deployments, add the plugin to your `dynamic-plugins.yaml` configuration and verify it loads in the NFS app shell. Check the browser console for extension registration logs. - ---- - -## 10. Common Gotchas - -1. **Import paths depend on your approach**: Direct-to-GA → import from root (`.`). Phased → import NFS from `./alpha`. Getting this wrong causes silent failures. - -2. **TranslationBlueprint must target `pluginId: 'app'`**: Putting translations in the plugin itself won't work. They must be in a separate `createFrontendModule({ pluginId: 'app' })`. - -3. **Nav items require `title` + `icon` + `routeRef` on the page**: Nav entries are auto-discovered from `PageBlueprint` extensions. If your plugin's nav item isn't appearing, ensure all three params are set. `NavItemBlueprint` was removed in recent Backstage versions -- see `references/api-changes.md`. - -4. **Entity content not showing on entity pages**: Ensure `path`, `title`, and `loader` are all set on `EntityContentBlueprint`. The blueprint declares its own attach point — it works directly in the plugin's `extensions` array. - -5. **ApiBlueprint uses `defineParams` callback**: Don't pass the factory directly -- wrap it: `params: defineParams => defineParams(createApiFactory(...))`. - -6. **Keep component imports on `@backstage/core-plugin-api`**: Hooks like `useApi()` and `useRouteRef()` from `core-plugin-api` work in both legacy and NFS. Don't migrate them to `frontend-plugin-api` if you support legacy consumers. Only use `compatWrapper()` when a component depends on legacy context providers (e.g. old `SidebarContext`). - -7. **Drawer content only renders when active**: If your drawer needs initialization logic on mount, use `AppRootElementBlueprint` for the persistent part. - -8. **Module federation sharing**: Host and remote apps must share the same `@backstage/plugin-app-react` instance. Version mismatches cause runtime errors. - -9. **NFS page components must not include a page shell**: The framework provides the header via `PageLayout`. If your NFS component wraps content in `PageWithHeader` or `Page` + `Header`, you'll get double headers. Create an `NfsMyPage` variant without the shell — see `references/migrate-page.md` for the dual header pattern. - -10. **`useRouteRef` returns `undefined` in NFS**: The NFS `useRouteRef` from `@backstage/frontend-plugin-api` returns `RouteFunc | undefined` (the route might not be bound). The legacy version from `core-plugin-api` throws instead. When writing NFS-specific components, handle the `undefined` case. - ---- - -## 11. Recent API Changes - -If you migrated a plugin against an earlier Backstage NFS alpha, some APIs have changed. Key changes include the removal of `NavItemBlueprint`, deprecation of `makeWithOverrides` config pattern, and new params on `PageBlueprint` and `createFrontendPlugin`. - -See the full list in [references/api-changes.md](../skills/nfs-migration/references/api-changes.md). - ---- - -## 12. Automate It - -Instead of migrating manually, use the included Agent Skill: - -```bash -npx skills add redhat-developer/rhdh-skill --skill nfs-migration -``` - -Then tell your agent: *"Migrate my plugin to NFS"* -- it will analyze your plugin, apply the right patterns, update exports, and verify the result. - -See [skills/nfs-migration/SKILL.md](../skills/nfs-migration/SKILL.md) for details. - ---- - -## 13. Reference PRs - -Real RHDH plugin migrations to study: - -| Plugin | PR | What to learn | -|--------|-----|---------------| -| adoption-insights | [#2309](https://github.com/redhat-developer/rhdh-plugins/pull/2309) | Simple page plugin: Page + Nav + API | -| bulk-import | [#2247](https://github.com/redhat-developer/rhdh-plugins/pull/2247) | Page + Nav + permission patterns | -| scorecard | [#2487](https://github.com/redhat-developer/rhdh-plugins/pull/2487) | EntityContent + HomePage widgets | -| orchestrator | [#2526](https://github.com/redhat-developer/rhdh-plugins/pull/2526) | EntityContent + multi-route | -| lightspeed | [#2721](https://github.com/redhat-developer/rhdh-plugins/pull/2721) | Drawer + FAB (RHDH-specific) | -| extensions | [#2527](https://github.com/redhat-developer/rhdh-plugins/pull/2527) | compatWrapper usage | -| homepage | [#2423](https://github.com/redhat-developer/rhdh-plugins/pull/2423) | HomePageWidgets + compatWrapper | -| quickstart | [#2842](https://github.com/redhat-developer/rhdh-plugins/pull/2842) | Drawer + GlobalHeaderMenuItem | - -### Upstream Backstage Docs - -- [Plugin migration guide](https://backstage.io/docs/frontend-system/building-plugins/migrating) -- [Common extension blueprints](https://backstage.io/docs/frontend-system/building-plugins/common-extension-blueprints) -- [App migration guide](https://backstage.io/docs/frontend-system/building-apps/migrating) - ---- - -## 14. Need Help? - -- [RHDH Plugins GitHub Issues](https://github.com/redhat-developer/rhdh-plugins/issues) -- [Backstage Discord](https://discord.gg/backstage-687207715902193673) -- [Backstage Community](https://backstage.io/community/) -- [RHDH Documentation](https://docs.redhat.com/en/documentation/red_hat_developer_hub/) diff --git a/skills/nfs-migration/SKILL.md b/skills/nfs-migration/SKILL.md index 5428a23..9754295 100644 --- a/skills/nfs-migration/SKILL.md +++ b/skills/nfs-migration/SKILL.md @@ -10,16 +10,14 @@ description: > to the new frontend system for RHDH. --- -> **Human-readable guide:** `docs/nfs-migration-guide.md` is the authoritative source for migration patterns. These reference files are optimized for agent consumption. When patterns diverge, the guide takes precedence. - Always read the plugin's `package.json`, `src/plugin.ts` (or `src/plugin.tsx`), route refs, API factories, and exported components before making any changes. Understand what exists before migrating. - -NFS should be the root export (`.`). Legacy goes to `./legacy` with `@deprecated` tags if kept. This is the GA pattern. + +NFS is not GA yet. The default approach is to add NFS at `./alpha` while keeping legacy at the root export (`.`). This avoids breaking existing consumers. @@ -31,11 +29,11 @@ Entity content and cards can go directly in the plugin's `extensions` array — -Keep component imports (`useApi`, `useRouteRef`, etc.) on `@backstage/core-plugin-api` — they work in both legacy and NFS contexts. This lets the same components serve both the root export (NFS) and `./legacy` export. Only use `compatWrapper()` when a component depends on legacy context providers (e.g. old `SidebarContext`) that aren't available in NFS. Don't migrate component imports to `@backstage/frontend-plugin-api` if you need to support legacy consumers. +Keep component imports (`useApi`, `useRouteRef`, etc.) on `@backstage/core-plugin-api` — they work in both legacy and NFS contexts. This lets the same components serve both export paths. Only use `compatWrapper()` when a component depends on legacy context providers (e.g. old `SidebarContext`) that aren't available in NFS. Don't migrate component imports to `@backstage/frontend-plugin-api` if you need to support legacy consumers. - -Ask the user if they want to keep legacy exports at `./legacy`. If yes, move old `plugin.ts` code there with `@deprecated` JSDoc. If no, remove it. + +Legacy exports must remain available since NFS is not GA. With the alpha approach, legacy stays at root unchanged. With the colocated approach, legacy source moves to `legacy.ts` but is re-exported from `index.ts` so existing consumers don't break. @@ -58,7 +56,7 @@ Ask the user if they want to keep legacy exports at `./legacy`. If yes, move old |----------|--------| | 1, "migrate", "convert", "NFS" | Follow the migration workflow below | | 2, "test", "verify", "deploy" | Read `workflows/test-nfs-plugin.md` | -| 3, "learn", "guide", "overview" | Read `../../docs/nfs-migration-guide.md` and present key sections to the user | +| 3, "learn", "guide", "overview" | Read `references/support.md` for upstream Backstage docs and resources | @@ -83,9 +81,9 @@ If the plugin's `@backstage/*` dependencies are outdated, upgrade them first usi ### Step 2: Choose Approach -Use **Direct to GA** by default: NFS becomes root export (`.`), legacy at `./legacy`. +NFS is not GA yet. Use the **Alpha** approach by default: NFS at `./alpha`, legacy stays at root (`.`). -Only ask about the **Phased** approach (`./alpha`) if the user says they have external consumers that can't migrate yet. +The **Colocated** approach is the alternative: NFS as default export in `index.ts`, legacy source in `legacy.ts` but re-exported from `index.ts` for backward compatibility. Use this when the user wants NFS and legacy APIs available from the same import path. ### Step 3: Migrate Extensions @@ -103,14 +101,14 @@ Apply each reference's patterns to the discovered extensions. For page plugins, ### Step 4: Update package.json -Load `references/package-json.md` and apply the export configuration matching the chosen approach (GA or phased). +Load `references/package-json.md` and apply the export configuration matching the chosen approach (alpha or colocated). ### Step 5: Update App Wiring Load `references/app-setup.md` and: -- Convert `dev/index.tsx` to use the NFS dev app pattern (`createDevApp` from `@backstage/frontend-dev-utils`) -- Move the old legacy dev app to `dev/legacy.tsx` with a `start:legacy` script -- If `packages/app` imports legacy APIs from the plugin root, update those imports to use the `./legacy` subpath (or create a separate `packages/app-legacy` for the old frontend system, keeping `packages/app` as NFS) +- Add an NFS dev app at `dev/index.tsx` (or `dev/nfs.tsx`) using `createApp` from `@backstage/frontend-defaults` +- Keep the existing legacy dev app working +- Verify consumer imports still resolve (alpha approach: no changes needed; colocated approach: legacy re-exports from `index.ts` maintain compatibility) ### Step 6: Verify @@ -135,20 +133,20 @@ Load `references/verification.md` and run all checks. Run `yarn tsc` from the ** | `references/testing-rhdh.md` | Testing with a real RHDH instance | | `references/gotchas.md` | Troubleshooting migration issues | | `references/reference-prs.md` | Looking for real migration examples | +| `references/operator-config.md` | Plugin uses RHDH operator config or needs `app.extensions` / `app.routes.bindings` reference | | `references/support.md` | User needs help beyond what the skill covers | -| `../../docs/nfs-migration-guide.md` | User wants to learn about NFS | -- Plugin default-exports a `createFrontendPlugin` result +- `./alpha` (or root, for colocated) default-exports a `createFrontendPlugin` result - All legacy extensions have NFS Blueprint equivalents - Pages that need nav entries have `title` and `icon` set (on `PageBlueprint` or `createFrontendPlugin`) -- `package.json` exports NFS at `.` (direct-to-GA) or `./alpha` (phased) +- `package.json` exports NFS at `./alpha` (alpha approach) or `.` (colocated approach) - Translations are in a `createFrontendModule` with `pluginId: 'app'` - Entity content extensions are in the plugin's `extensions` array (or a catalog module if injecting from outside) - `yarn tsc` and `yarn build` pass -- Legacy code is at `./legacy` with `@deprecated` tags (if kept) +- Legacy exports remain available (unchanged at root for alpha; re-exported from `index.ts` for colocated) diff --git a/skills/nfs-migration/references/api-changes.md b/skills/nfs-migration/references/api-changes.md index 0f01f98..b2cc2b5 100644 --- a/skills/nfs-migration/references/api-changes.md +++ b/skills/nfs-migration/references/api-changes.md @@ -6,7 +6,7 @@ Breaking and notable changes between the early NFS alpha and the current Backsta ## Component imports — keep on `core-plugin-api` -Hooks like `useApi`, `useRouteRef`, and `useRouteRefParams` from `@backstage/core-plugin-api` work in both legacy and NFS contexts. **Keep component imports on `core-plugin-api`** so the same components serve both the root NFS export and the `./legacy` export. +Hooks like `useApi`, `useRouteRef`, and `useRouteRefParams` from `@backstage/core-plugin-api` work in both legacy and NFS contexts. **Keep component imports on `core-plugin-api`** so the same components serve both export paths. Only the plugin definition code (`plugin.tsx`) and blueprint/API factory code use `@backstage/frontend-plugin-api` imports. Don't migrate component-level imports — it breaks legacy consumers. diff --git a/skills/nfs-migration/references/app-setup.md b/skills/nfs-migration/references/app-setup.md index 9c3b727..c28646b 100644 --- a/skills/nfs-migration/references/app-setup.md +++ b/skills/nfs-migration/references/app-setup.md @@ -42,7 +42,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(App.createRoot()); ## Dev app setup -For direct-to-GA, `dev/index.tsx` should be the NFS dev app (it's the default `yarn start` entry point). Keep the old legacy dev app at `dev/legacy.tsx` and add a `start:legacy` script. +Add an NFS dev app alongside the existing legacy dev app. Keep the legacy dev app as the default `yarn start` entry point (since NFS is not GA). Add the NFS dev app at `dev/nfs.tsx` with a `start:nfs` script, or at `dev/index.tsx` if you prefer NFS as default during development. ### NFS dev app (`dev/index.tsx`) @@ -96,12 +96,12 @@ Move the old `createDevApp` from `@backstage/dev-utils` code here. Add to `packa "start:legacy": "backstage-cli package start --entrypoint dev/legacy" ``` -## Consumer migration (packages/app) +## Consumer imports -If the workspace has a `packages/app` that imports legacy APIs from the plugin's root, those imports will break after the GA migration (legacy is no longer at the root export). Two approaches: +Since NFS is not GA, legacy exports must remain accessible from the package root: -1. **Update imports** — Change `import { MyPage } from '@scope/my-plugin'` to `import { MyPage } from '@scope/my-plugin/legacy'` -2. **Create a separate legacy app** — Keep `packages/app` as the NFS app and create `packages/app-legacy` for the old frontend system. This is the pattern used in `rhdh-plugins`. +- **Alpha approach:** No consumer changes needed — legacy stays at root, NFS is at `./alpha`. +- **Colocated approach:** Legacy is re-exported from `index.ts` — existing imports continue to work. NFS consumers use the default import. ## Dynamic plugin considerations (RHDH) diff --git a/skills/nfs-migration/references/gotchas.md b/skills/nfs-migration/references/gotchas.md index 8bb8fbd..ae19594 100644 --- a/skills/nfs-migration/references/gotchas.md +++ b/skills/nfs-migration/references/gotchas.md @@ -103,19 +103,18 @@ export default createFrontendPlugin({ pluginId: 'my-plugin', ... }); **Fix:** Use `plugin.tsx` (not `.ts`) for the NFS plugin file. Imports like `from './plugin'` resolve both extensions automatically. -## 12. Re-exporting legacy APIs from the root `index.ts` +## 12. Forgetting to keep legacy exports accessible -**Why:** For direct-to-GA, the root export (`.`) should be NFS-only. If you re-export legacy named exports from `index.ts`, consumers get both APIs from the same path, which defeats the purpose of the GA migration. +**Why:** NFS is not GA yet. Existing consumers import legacy APIs from the package root. If you remove or move those exports without re-exporting them, consumers break. -**Fix:** Legacy exports should only be reachable via the `./legacy` subpath: +**Fix:** Depends on approach: +- **Alpha approach:** Legacy stays at root — no changes needed. +- **Colocated approach:** Legacy source moves to `legacy.ts`, but must be re-exported from `index.ts`: ```tsx -// src/index.ts — NFS only +// src/index.ts — NFS default + legacy re-exports export { default } from './plugin'; export { isMyPluginAvailable } from './utils'; -// Do NOT re-export legacy APIs here - -// src/legacy.ts — legacy only, reachable via '@scope/my-plugin/legacy' -export { myPlugin, MyPage } from './legacyPlugin'; +export { myPlugin, MyPage } from './legacy'; // backward compat ``` ## 13. Double headers in NFS pages @@ -139,3 +138,70 @@ const link = useRouteRef(myRouteRef); // link might be undefined — check before calling const href = link?.() ?? '/fallback'; ``` + +## 15. Config file merging replaces arrays, not merges + +**Why:** Backstage merges config files by **replacing entire arrays**. If `app.extensions` appears in multiple config files, the higher-priority file's array replaces the lower-priority one — entries are not merged entry-by-entry. Individual extension `config` objects are also replaced wholesale. + +**Fix:** Because unlisted extensions are still auto-discovered, a local override file can contain only the extensions you want to change. But verify merge order when overrides seem to have no effect. See `operator-config.md`. + +## 16. Dependencies tab removed in NFS + +**Why:** The legacy `/dependencies` tab (`entity.page.dependencies`) does not exist in upstream NFS. The dependency cards are now available as individual overview extensions. + +**Fix:** Enable the individual cards instead: +- `entity-card:catalog/depends-on-components` +- `entity-card:catalog/depends-on-resources` +- `entity-card:catalog/has-subcomponents` +- `entity-card:api-docs/provided-apis` +- `entity-card:api-docs/consumed-apis` +- `entity-card:catalog-graph/relations` + +To restore a dedicated Dependencies tab, a plugin must contribute a custom `entity-content:*` extension. + +## 17. System diagram tab replaced + +**Why:** The legacy `/system` tab (`entity.page.diagram`) with a full-width catalog graph does not exist in NFS. + +**Fix:** Use `entity-card:catalog-graph/relations` on the overview for a compact graph. The "View Graph" action opens `page:catalog-graph` for a standalone full view. Configure the card for richer graphs via `app.extensions` (height, direction, relations list). + +## 18. API tab path changed + +**Why:** `entity-content:api-docs/apis` is at path `/apis` (with an **s**), changed from the legacy `/api`. + +**Fix:** Update bookmarks, documentation links, and any hardcoded paths. + +## 19. Overview layout model change + +**Why:** NFS uses `DefaultEntityContentLayout` with two card types: `type: info` (sticky sidebar) and `type: content` (main area). This replaces the legacy per-entity-kind MUI grid layout. Warnings (orphan, relation, processing errors) are built into the layout. + +**Fix:** Configure cards via `app.extensions`. See `migrate-entity-content.md` for the `type: info` vs `type: content` distinction. + +## 20. RHDH-specific features without upstream NFS equivalents + +These features have no direct NFS equivalent yet: + +| Feature | Status | +|---------|--------| +| Nested sidebar menu groups (`menuItems.parent`) | No equivalent — use `NavContentBlueprint` for custom nav | +| Application drawer mount points | RHDH-specific — pending NFS design | +| `global.header/help` and similar header slots | Being migrated in `rhdh-plugins` global-header workspace | +| `mountPoints[].config.layout` (MUI grid SX) | Not configurable via YAML — implement in component CSS | +| `staticJSXContent` dynamic plugin pattern | Legacy dynamic host — replace with extension inputs / Utility APIs | + +## 21. Things you cannot do from configuration alone + +- Attach arbitrary exported components to mount points without a matching NFS extension from the plugin. +- Replicate `mountPoints[].config.layout` grid column positioning — use card `type: info`/`type: content` or adjust the component layout. +- Add a new entity tab without a plugin that exports `entity-content:*`. +- Add cards to General settings until upstream exposes extension inputs on `sub-page:user-settings/general`. +- Use RHDH-only mount points (application drawers, some global header slots) until equivalent NFS extensions exist. + +## 22. User settings page limitations in NFS + +**Why:** The upstream `user-settings` plugin has limited extension inputs: +- `page:user-settings` can be overridden via `app.extensions` (interim escape hatch) +- `sub-page:user-settings/general` has **no card/content input** — you cannot add cards +- Default settings cards are **not individual extensions** — you cannot hide them selectively + +**Fix:** Wait for upstream plugin updates, or replace the full settings page via `page:user-settings` override. diff --git a/skills/nfs-migration/references/migrate-app-level.md b/skills/nfs-migration/references/migrate-app-level.md index cf82039..6efeba0 100644 --- a/skills/nfs-migration/references/migrate-app-level.md +++ b/skills/nfs-migration/references/migrate-app-level.md @@ -34,6 +34,20 @@ const appElement = AppRootElementBlueprint.make({ }); ``` +## PluginWrapperBlueprint — wraps a single plugin's UI + +Use for context providers that should only wrap one plugin, not the entire app. Imported from `@backstage/frontend-plugin-api/alpha`. + +```tsx +import { PluginWrapperBlueprint } from '@backstage/frontend-plugin-api/alpha'; + +const myPluginWrapper = PluginWrapperBlueprint.make({ + params: { component: MyPluginProvider }, +}); +``` + +Unlike `AppRootWrapperBlueprint` (app-wide), this scopes the provider to your plugin's extensions only. + ## Shared components (legacy + NFS) Hooks like `useApi` and `useRouteRef` from `@backstage/core-plugin-api` work in both legacy and NFS contexts. Keep component imports on `core-plugin-api` so the same components serve both export paths: @@ -61,6 +75,7 @@ Import `compatWrapper` from `@backstage/core-compat-api`. Most plugins won't nee | Need invisible element at root (init, snackbars, FABs) | `AppRootElementBlueprint` | | Components using `useApi`/`useRouteRef` | Keep on `@backstage/core-plugin-api` — works in both systems | | Component depends on legacy context providers | Wrap with `compatWrapper()` (rare) | +| Provider scoped to one plugin only | `PluginWrapperBlueprint` | | Both wrapping and init logic needed | Use both separately — don't combine | All app-level extensions go in your plugin's `extensions` array (they belong to your plugin, not to another plugin). diff --git a/skills/nfs-migration/references/migrate-entity-content.md b/skills/nfs-migration/references/migrate-entity-content.md index ffda6c6..c0df1c4 100644 --- a/skills/nfs-migration/references/migrate-entity-content.md +++ b/skills/nfs-migration/references/migrate-entity-content.md @@ -87,3 +87,107 @@ export const myCatalogModule = createFrontendModule({ ``` Export the module so consumers can include it in their app's `features` array. + +## EntityContextMenuItemBlueprint — entity context menu actions + +Replaces `entity.context.menu` mount point. Uses a data-driven approach instead of dialog wrapper components. + +```tsx +import { EntityContextMenuItemBlueprint } from '@backstage/plugin-catalog-react/alpha'; + +const myMenuItem = EntityContextMenuItemBlueprint.make({ + name: 'my-action', + params: { + icon: , + title: 'Run Action', + onClick: ({ entity, dialogApi }) => { /* handle action */ }, + filter: entity => entity.kind === 'Component', + }, +}); +``` + +Legacy `entity.context.menu` items were often dialog wrappers with `open`/`onClose` props — refactor the dialog lifecycle into the `onClick` handler. + +## Entity tab groups + +Tabs are organized into groups on `page:catalog/entity`. Default groups: `overview`, `documentation`, `development`, `deployment`, `operation`, `observability`. + +**Plugin authors** assign content to a group via the `group` param: + +```tsx +const entityContent = EntityContentBlueprint.make({ + name: 'my-tab', + params: { + path: '/my-tab', + title: 'My Tab', + group: 'development', + loader: () => import('./MyTab').then(m => ), + }, +}); +``` + +**Operators** configure groups in `app-config.yaml`: + +```yaml +app: + extensions: + - page:catalog/entity: + config: + showNavItemIcons: true + groups: + - overview: + title: Overview + - documentation: + title: Documentation + - development: + title: Development + - custom: + title: My Custom Group + - deployment: false # hide this group +``` + +Set `group: false` on an `entity-content:*` extension to show it as a standalone tab outside any group. + +See `operator-config.md` for the full operator configuration reference. + +## Card layout: `type: info` vs `type: content` + +Entity overview uses `DefaultEntityContentLayout` with two card types: + +- **`type: info`** — renders in a sticky sidebar (right side). Use for compact summary cards like About, Links. +- **`type: content`** — renders in the main area (left side). Default if not specified. + +```yaml +app: + extensions: + - entity-card:catalog/about: + config: + type: info + - entity-card:catalog/links: + config: + type: info +``` + +Warnings (orphan, relation, processing errors) are built into the layout — no separate mount point configuration needed. + +## Built-in entity extension IDs + +**Overview cards** (attach to `entity-content:catalog/overview`): + +- `entity-card:catalog/about`, `entity-card:catalog/labels`, `entity-card:catalog/links` +- `entity-card:catalog-graph/relations` +- `entity-card:catalog/depends-on-components`, `entity-card:catalog/depends-on-resources` +- `entity-card:catalog/has-subcomponents`, `entity-card:catalog/has-components` +- `entity-card:catalog/has-resources`, `entity-card:catalog/has-systems` +- `entity-card:api-docs/has-apis`, `entity-card:api-docs/consumed-apis`, `entity-card:api-docs/provided-apis` +- `entity-card:api-docs/providing-components`, `entity-card:api-docs/consuming-components` +- `entity-card:org/group-profile`, `entity-card:org/members-list`, `entity-card:org/ownership`, `entity-card:org/user-profile` + +**Content tabs** (attach to `page:catalog/entity`): + +- `entity-content:catalog/overview` +- `entity-content:api-docs/definition`, `entity-content:api-docs/apis` +- `entity-content:techdocs` +- `entity-content:kubernetes/kubernetes` + +Additional tabs (CI, CD, Argo CD, etc.) appear when the corresponding dynamic plugin exports an `entity-content:*` extension. diff --git a/skills/nfs-migration/references/migrate-rhdh-extensions.md b/skills/nfs-migration/references/migrate-rhdh-extensions.md index 54a99cb..2db596d 100644 --- a/skills/nfs-migration/references/migrate-rhdh-extensions.md +++ b/skills/nfs-migration/references/migrate-rhdh-extensions.md @@ -110,3 +110,154 @@ const myWrapper = AppRootWrapperBlueprint.make({ ``` Register via `createFrontendModule({ pluginId: 'app' })`. + +## IconBundleBlueprint — custom icon sets + +Replaces `appIcons` config. Registers multiple icons for use across the app (e.g. as string IDs in `config.icon` on page and entity-content extensions). + +```tsx +import { IconBundleBlueprint } from '@backstage/plugin-app-react'; + +const myIcons = IconBundleBlueprint.make({ + params: { + icons: { + fooIcon: , + barIcon: , + }, + }, +}); +``` + +Add to `createFrontendPlugin({ extensions: [...] })`. Icons are auto-discovered with the plugin. For a single icon on one page, using the `icon` param on `PageBlueprint` is simpler. + +## ThemeBlueprint — custom themes + +Replaces legacy `themes` config with `id`, `title`, `variant`, `importName`. + +```tsx +import { ThemeBlueprint } from '@backstage/plugin-app-react'; +import { lightTheme } from './lightTheme'; + +const customLightTheme = ThemeBlueprint.make({ + name: 'light', + params: { + theme: lightTheme, + title: 'Light', + variant: 'light', + icon: , + }, +}); +``` + +Use `name: 'light'` or `name: 'dark'` to override the built-in themes. Adopters can override the title via `app.extensions`: + +```yaml +app: + extensions: + - theme:my-plugin/light: + config: + title: Corporate Light +``` + +## SignInPageBlueprint — custom sign-in pages + +Replaces legacy `signInPage` config with `importName`. + +```tsx +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; + +const customSignIn = SignInPageBlueprint.make({ + params: { + loader: async () => { + const { CustomSignInPage } = await import('./CustomSignInPage'); + return CustomSignInPage; + }, + }, +}); +``` + +Only one sign-in page extension can be active. Installing multiple replaces based on extension override rules. + +## FormFieldBlueprint — custom scaffolder fields + +Replaces legacy `scaffolderFieldExtensions` config with `importName`. + +```tsx +import { FormFieldBlueprint } from '@backstage/plugin-scaffolder-react/alpha'; + +export const myField = FormFieldBlueprint.make({ + name: 'MyCustomField', + params: { + schema: { /* JSON schema fragment */ }, + loader: async () => { + const { MyCustomField } = await import('./MyCustomField'); + return MyCustomField; + }, + }, +}); +``` + +Fields are auto-discovered via `formFieldsApiRef` when the plugin is installed. No YAML registration needed — template authors use the field name in `template.yaml` as before. + +## AddonBlueprint — TechDocs addons + +Replaces legacy `techdocsAddons` config with `importName`. + +```tsx +import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; +import { TechDocsAddonLocations } from '@backstage/plugin-techdocs-react'; + +const exampleAddon = AddonBlueprint.make({ + name: 'example', + params: { + location: TechDocsAddonLocations.Content, + component: ExampleAddon, + }, +}); +``` + +Addons are collected via `techdocsAddonsApiRef` and merged into TechDocs reader and entity content extensions automatically. The `staticJSXContent` pattern from legacy dynamic plugins is no longer needed. + +## NavContentBlueprint — custom sidebar layout + +Replaces the entire sidebar navigation component. Use this as an escape hatch when you need custom navigation structure (e.g. RHDH nested `menuItems.parent` groups, which have no direct NFS equivalent). + +```tsx +import { NavContentBlueprint } from '@backstage/plugin-app-react'; + +const customNav = NavContentBlueprint.make({ + params: { + component: MyCustomSidebar, + }, +}); +``` + +Most plugins don't need this — standard page auto-discovery provides sidebar items. Only use when you need non-standard sidebar structure like nested groups. + +## Custom auth provider settings + +No dedicated blueprint — use `createExtension` directly, targeting the user-settings auth providers sub-page: + +```tsx +import { createExtension, coreExtensionData } from '@backstage/frontend-plugin-api'; +import { ProviderSettingsItem } from '@backstage/plugin-user-settings'; + +const myProviderSettings = createExtension({ + kind: 'auth-provider-settings', + name: 'my-custom', + attachTo: { id: 'sub-page:user-settings/auth-providers', input: 'providerSettings' }, + output: [coreExtensionData.reactElement], + factory: () => [ + coreExtensionData.reactElement( + , + ), + ], +}); +``` + +Also provide `ApiBlueprint` for the auth API and `SignInPageBlueprint` if the provider needs a custom sign-in flow. diff --git a/skills/nfs-migration/references/mount-point-mapping.md b/skills/nfs-migration/references/mount-point-mapping.md index 8d0e4ca..fc8fa13 100644 --- a/skills/nfs-migration/references/mount-point-mapping.md +++ b/skills/nfs-migration/references/mount-point-mapping.md @@ -16,7 +16,12 @@ RHDH's legacy dynamic plugin system used mount points in `app-config.dynamic.yam | `application/internal/drawer-state` | Init logic in `AppRootElementBlueprint` | `@backstage/frontend-plugin-api` | | `global.header/*` | `GlobalHeaderMenuItemBlueprint` | `@red-hat-developer-hub/backstage-plugin-global-header/alpha` | | `header/component`, `header/*` | `GlobalHeaderMenuItemBlueprint` | `@red-hat-developer-hub/backstage-plugin-global-header/alpha` | -| `appIcons` | `icon` param on `createFrontendPlugin` | — | +| `appIcons` | `IconBundleBlueprint` (or `icon` param on `createFrontendPlugin` for single icons) | `@backstage/plugin-app-react` | +| `entity.context.menu` | `EntityContextMenuItemBlueprint` | `@backstage/plugin-catalog-react/alpha` | +| `search.page.results` | `SearchResultListItemBlueprint` | `@backstage/plugin-search-react/alpha` | +| `search.page.filters` | `SearchFilterBlueprint` | `@backstage/plugin-search-react/alpha` | +| `search.page.types` | `SearchFilterResultTypeBlueprint` | `@backstage/plugin-search-react/alpha` | +| `application/header` | `AppRootElementBlueprint` | `@backstage/plugin-app-react` | ## Dynamic routes → PageBlueprint @@ -205,6 +210,113 @@ const myMenuItem = GlobalHeaderMenuItemBlueprint.make({ The `target` param maps to the header section: `help`, `profile`, `create`, etc. Use `priority` to control ordering. +## Entity context menu → EntityContextMenuItemBlueprint + +**Before (mount point):** +```yaml +mountPoints: + - mountPoint: entity.context.menu + importName: SimpleDialog + config: + props: + title: Open Dialog + icon: dialogIcon +``` + +**After (NFS):** +```tsx +import { EntityContextMenuItemBlueprint } from '@backstage/plugin-catalog-react/alpha'; + +const myMenuItem = EntityContextMenuItemBlueprint.make({ + name: 'my-action', + params: { + icon: , + title: 'Open Dialog', + onClick: ({ entity, dialogApi }) => { /* open dialog */ }, + filter: entity => entity.kind === 'Component', + }, +}); +``` + +The new system uses data-driven menu items, not dialog wrapper components with `open`/`onClose` props. Refactor dialog lifecycle into the `onClick` handler. + +## Search page results → SearchResultListItemBlueprint + +**Before (mount point):** +```yaml +mountPoints: + - mountPoint: search.page.results + importName: MySearchResultItem +``` + +**After (NFS):** +```tsx +import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; + +const mySearchItem = SearchResultListItemBlueprint.make({ + params: { + predicate: result => result.type === 'my-type', + component: async () => { + const { MyResultItem } = await import('./MyResultItem'); + return props => ; + }, + }, +}); +``` + +## Search page filters → SearchFilterBlueprint + +**Before (mount point):** +```yaml +mountPoints: + - mountPoint: search.page.filters + importName: MySearchFilter +``` + +**After (NFS):** +```tsx +import { SearchFilterBlueprint } from '@backstage/plugin-search-react/alpha'; + +const myFilter = SearchFilterBlueprint.make({ + params: { + loader: async () => { + const { MySearchFilter } = await import('./MySearchFilter'); + return props => ; + }, + }, +}); +``` + +`SearchFilterResultTypeBlueprint` (from the same package) follows the same pattern for `search.page.types` mount points. + +## Application header → AppRootElementBlueprint + +**Before (mount point):** +```yaml +mountPoints: + - mountPoint: application/header + importName: GlobalHeader + config: + position: above-main-content +``` + +**After (NFS):** +```tsx +import { AppRootElementBlueprint } from '@backstage/plugin-app-react'; + +const myHeader = AppRootElementBlueprint.make({ + name: 'my-header', + params: { + loader: async () => { + const { GlobalHeader } = await import('./GlobalHeader'); + return ; + }, + }, +}); +``` + +RHDH's global header plugin is being migrated to extension blueprints in `rhdh-plugins`. The `position: above-main-content` concept is app-layout-specific — verify layout behavior when migrating. + ## Real migration examples | Plugin | Mount points used | NFS blueprints | PR | diff --git a/skills/nfs-migration/references/operator-config.md b/skills/nfs-migration/references/operator-config.md new file mode 100644 index 0000000..a8e06bb --- /dev/null +++ b/skills/nfs-migration/references/operator-config.md @@ -0,0 +1,123 @@ +# Operator Configuration Reference (New Frontend System) + +Operators and platform admins customize NFS apps through `app-config.yaml` keys, not plugin code. This reference covers the configuration surface. For plugin-author migration, see the other reference files. + +## `app.extensions` + +The primary tool for enabling, disabling, reordering, and configuring extensions. + +### Resolution rules + +1. All extensions from installed plugins are **auto-discovered** and loaded by default. +2. Entries in `app.extensions` **override** matching extensions by ID. +3. Extensions **listed** in `app.extensions` are **reordered** to appear first, in list order. Unlisted extensions keep their default order afterward. + +You typically list only extensions you want to customize — not the full inventory. + +### Extension ID format + +`[:][/]` — for example `page:catalog`, `entity-card:catalog/about`, `entity-content:techdocs`. + +### Syntax + +```yaml +app: + extensions: + # Shorthand: enable with defaults + - entity-card:catalog/about + + # Shorthand: disable + - page:catalog-unprocessed-entities: false + + # Full form with config + - entity-card:catalog/links: + config: + filter: + kind: component + type: info +``` + +### Config merging caveat + +Backstage merges config files by **replacing entire arrays**. If `app.extensions` appears in multiple config files, the higher-priority file's array **replaces** the lower-priority one — entries are not merged entry-by-entry. Individual extension `config` objects are also replaced wholesale when overridden. + +Because unlisted extensions are still auto-discovered, a local override file can contain only the extensions you want to change. + +## `app.routes.bindings` + +Replaces legacy `routeBindings` in `dynamicPlugins.frontend`. Uses `pluginId.routeName` syntax: + +```yaml +app: + routes: + bindings: + catalog.viewTechDoc: techdocs.docRoot + catalog.createComponent: scaffolder.index + scaffolder.registerComponent: false # disable a binding +``` + +See [Frontend Routes](https://backstage.io/docs/frontend-system/architecture/routes/#binding-external-route-references) for details. + +## `app.packages` + +Controls which frontend plugin packages are auto-discovered: + +```yaml +app: + packages: all +``` + +Or restrict explicitly: + +```yaml +app: + packages: + include: + - '@backstage/plugin-catalog' + - '@backstage/plugin-techdocs' + exclude: [] +``` + +Dynamic plugins loaded at runtime through the frontend feature loader are discovered separately from this setting. + +## Scaffolder template grouping + +Group templates on the scaffolder page using `sub-page:scaffolder/templates`: + +```yaml +app: + extensions: + - sub-page:scaffolder/templates: + config: + groups: + - title: Recommended Services + filter: + spec.type: service + - title: Internal Tools + filter: + spec.type: tool +``` + +## Operator cheat sheet + +| Task | Legacy RHDH | New frontend system | +|------|-------------|---------------------| +| Install a plugin | `dynamic-plugins.yaml` entry | Same — `enabled: true` | +| Disable a plugin page | Remove route or `menuItem.enabled: false` | `page:my-plugin: false` | +| Rename sidebar item | `menuItem.text` | `page:my-plugin` → `config.title` | +| Reorder sidebar | `menuItems.*.priority` | Order in `app.extensions` | +| Hide entity overview card | Remove mount point entry | `entity-card:*: false` | +| Change card visibility filter | `mountPoints[].config.if` | `entity-card:*` → `config.filter` | +| Rename entity tab | `entityTabs[].title` | `entity-content:*` → `config.title` | +| Reorder / group entity tabs | `entityTabs` + `priority` | `page:catalog/entity` → `config.groups` | +| Hide entity tab | Negative `entityTabs` priority | `entity-content:*: false` | +| Bind cross-plugin routes | `routeBindings` | `app.routes.bindings` | +| Disable route binding | Omit binding | `app.routes.bindings.: false` | + +## What you cannot do from configuration alone + +- **Attach arbitrary exported components** to mount points without a matching NFS extension from the plugin. +- **Replicate `mountPoints[].config.layout`** grid column positioning — use card `type: info`/`type: content` or adjust the component layout. +- **Add a new entity tab** without a plugin that exports `entity-content:*`. +- **Add cards to General settings** until upstream exposes extension inputs on `sub-page:user-settings/general`. +- **Use RHDH-only mount points** (application drawers, some global header slots) until equivalent NFS extensions exist. diff --git a/skills/nfs-migration/references/package-json.md b/skills/nfs-migration/references/package-json.md index 3570c48..6c7fded 100644 --- a/skills/nfs-migration/references/package-json.md +++ b/skills/nfs-migration/references/package-json.md @@ -1,62 +1,57 @@ # Package.json Export Configuration -## Direct to GA (recommended) +## Alpha approach (default) -NFS is the root export. Legacy moves to `./legacy`. +NFS at `./alpha`, legacy stays at root. No breaking changes for consumers. This is the recommended approach while NFS is not GA. ```json { "exports": { ".": "./src/index.ts", - "./legacy": "./src/legacy.ts", + "./alpha": "./src/alpha.tsx", "./package.json": "./package.json" }, "typesVersions": { "*": { - "legacy": ["src/legacy.ts"], + "alpha": ["src/alpha.tsx"], "package.json": ["package.json"] } - }, - "publishConfig": { - "access": "public", - "legacy": { - "types": "dist/legacy.d.ts", - "default": "dist/legacy.esm.js" - } } } ``` ### File layout -- `src/index.ts` — re-exports default from `plugin.tsx`, plus shared utilities (e.g. `isMyPluginAvailable`). **Do not re-export legacy APIs here** — they are only reachable via the `./legacy` subpath -- `src/plugin.tsx` — NFS plugin definition (`createFrontendPlugin` with blueprints, default export). Use `.tsx` since blueprint loaders return JSX -- `src/legacy.ts` — old `createPlugin(...)` result with `@deprecated` JSDoc tags +- `src/index.ts` — existing legacy exports (unchanged) +- `src/alpha.tsx` — default-exports `createFrontendPlugin(...)`, named-exports modules. Use `.tsx` since blueprint loaders return JSX -## Phased approach +## Colocated approach -NFS at `./alpha`, legacy stays at root. +NFS as default export in `index.ts`, legacy source in `legacy.ts` but re-exported from `index.ts` for backward compatibility. Use when you want both APIs available from the same import path. ```json { "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.tsx", "./package.json": "./package.json" - }, - "typesVersions": { - "*": { - "alpha": ["src/alpha.tsx"], - "package.json": ["package.json"] - } } } ``` ### File layout -- `src/index.ts` — existing legacy exports (unchanged) -- `src/alpha.tsx` — default-exports `createFrontendPlugin(...)`, named-exports modules +- `src/index.ts` — default-exports `createFrontendPlugin` from `plugin.tsx`, AND re-exports legacy named exports from `legacy.ts` for backward compatibility +- `src/plugin.tsx` — NFS plugin definition (`createFrontendPlugin` with blueprints, default export). Use `.tsx` since blueprint loaders return JSX +- `src/legacy.ts` — old `createPlugin(...)` result with `@deprecated` JSDoc tags + +```tsx +// src/index.ts (colocated approach) +export { default } from './plugin'; +export { isMyPluginAvailable } from './utils'; + +// Re-export legacy APIs for backward compatibility +export { myPlugin, MyPage, MyCard } from './legacy'; +``` ## Required backstage fields @@ -78,10 +73,50 @@ Ensure these exist in `package.json`: - `pluginId`: must match the `pluginId` passed to `createFrontendPlugin` - `pluginPackages`: array of all packages in this plugin family (frontend, backend, common, etc.) +## Scalprum configuration (dual-export period) + +RHDH dynamic plugins use a `scalprum` section in the derived package's `package.json` for the legacy Webpack module-federation container. Keep this working during migration. + +```json +{ + "scalprum": { + "name": "my-plugin-package", + "exposedModules": { + "PluginRoot": "./src/index.ts", + "FooModule": "./src/foo.ts" + } + } +} +``` + +- **`scalprum.name`** — Webpack container name. This is also the key under `dynamicPlugins.frontend` in operator YAML — it may differ from the npm package name. +- **`scalprum.exposedModules`** — maps module names to source entrypoints. Each key becomes a loadable entrypoint in the dynamic plugin bundle. + +Legacy wiring resolves modules via `module` (which `exposedModules` key to load, defaults to `PluginRoot`) and `importName` (which named export to render, defaults to default export): + +```yaml +dynamicPlugins: + frontend: + my-plugin-package: + mountPoints: + - mountPoint: entity.page.overview/cards + module: FooModule + importName: MyCard +``` + +The RHDH CLI `--scalprum-config` option can override this at export time. + +### Dual-export checklist + +- [ ] Legacy `scalprum.exposedModules` resolves all `importName`/`module` references in existing operator config +- [ ] `./alpha` export added for NFS (`createFrontendPlugin` default export) +- [ ] As each feature moves to NFS extensions, delete the corresponding `dynamicPlugins.frontend` YAML keys +- [ ] Re-export with a CLI version from the RHDH version matrix + ## Checklist -- [ ] `exports` field has `.` pointing to NFS entry +- [ ] `exports` field has `./alpha` (alpha approach) or `.` (colocated approach) pointing to NFS entry - [ ] `typesVersions` mirrors any sub-path exports -- [ ] `publishConfig` has types/default for each sub-path (GA approach) +- [ ] Legacy exports remain accessible (unchanged root for alpha; re-exported from `index.ts` for colocated) - [ ] `backstage.role` is `frontend-plugin` - [ ] `backstage.pluginId` matches `createFrontendPlugin({ pluginId: '...' })` diff --git a/skills/nfs-migration/references/support.md b/skills/nfs-migration/references/support.md index cd8dc48..213f2bd 100644 --- a/skills/nfs-migration/references/support.md +++ b/skills/nfs-migration/references/support.md @@ -8,6 +8,15 @@ - **[RHDH Documentation](https://docs.redhat.com/en/documentation/red_hat_developer_hub/)** — Official Red Hat Developer Hub documentation - **[Backstage NFS Docs](https://backstage.io/docs/frontend-system/)** — Upstream New Frontend System documentation, API reference, and migration guides +## Upstream Backstage docs + +- [Frontend System Introduction](https://backstage.io/docs/frontend-system/) +- [Migrating Plugins](https://backstage.io/docs/frontend-system/building-plugins/migrating/) +- [Migrating Apps](https://backstage.io/docs/frontend-system/building-apps/migrating/) +- [Configuring Extensions](https://backstage.io/docs/frontend-system/building-apps/configuring-extensions/) +- [Common Extension Blueprints](https://backstage.io/docs/frontend-system/building-plugins/common-extension-blueprints/) +- [Example `app-config.yaml`](https://github.com/backstage/backstage/blob/master/app-config.yaml) + ## When to escalate - **Build failures after migration** — Check `references/gotchas.md` first, then file an issue diff --git a/skills/nfs-migration/references/verification.md b/skills/nfs-migration/references/verification.md index 2a43b60..34101e1 100644 --- a/skills/nfs-migration/references/verification.md +++ b/skills/nfs-migration/references/verification.md @@ -53,16 +53,34 @@ Adapt selectors to your plugin. These are starting points, not production-ready | Shared components | Component imports stay on `core-plugin-api` (work in both legacy and NFS) | | Legacy compat | Components using `compatWrapper` (if any) render without errors | +## Blueprint-type-specific verification + +| Blueprint | What to verify | +|-----------|---------------| +| `FormFieldBlueprint` | Template author can use the custom field name in `template.yaml`; field renders in scaffolder form | +| `AddonBlueprint` | Addon renders in TechDocs reader at the specified location | +| `ThemeBlueprint` | Theme appears in user settings theme picker; overrides apply | +| `SignInPageBlueprint` | Custom sign-in page renders on unauthenticated access | +| `IconBundleBlueprint` | Icon IDs resolve in `config.icon` references on pages and entity content | + +## Scalprum / dynamic plugin verification + +If dual-exporting (legacy + NFS), also verify: + +- [ ] `scalprum.exposedModules` still resolves all `importName`/`module` references in existing operator config +- [ ] OCI image rebuilt and smoke-tested in RHDH with `APP_CONFIG_app_packageName=app-next` + ## Consumer import check -After migrating, verify that any workspace apps (`packages/app`, dev apps) that import from the plugin still compile. Legacy consumers must update their imports to use the `./legacy` subpath: +After migrating, verify that any workspace apps (`packages/app`, dev apps) that import from the plugin still compile: ```bash -# Find any imports of legacy named exports from the plugin's root +# Find all imports from the plugin grep -r "from '@scope/my-plugin'" packages/ --include='*.ts' --include='*.tsx' ``` -If hits reference legacy exports (e.g. `MyPage`, `myPlugin`), update them to import from `'@scope/my-plugin/legacy'`. +- **Alpha approach:** No changes needed — legacy is still at the root. +- **Colocated approach:** Legacy re-exports from `index.ts` maintain compatibility — verify they resolve. ## Quick validation commands