diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3d1f67c..e139e26 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Orchestrator skill for RHDH plugin development - onboard, update, and maintain plugins in the Extensions Catalog", - "version": "0.5.0" + "version": "0.6.0" }, "plugins": [ { "name": "rhdh", "source": "./", "description": "Skills for RHDH plugin lifecycle management", - "version": "0.5.0", + "version": "0.6.0", "strict": true } ] diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 758136d..eadcf3d 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "rhdh", "description": "All-in-one toolkit for Red Hat Developer Hub (RHDH). Covers plugin development, overlay management, environment setup, version compatibility, CI/CD, and RHDH ecosystem navigation.", - "version": "0.5.0", + "version": "0.6.0", "author": { "name": "RHDH Store Manager" }, diff --git a/README.md b/README.md index fbbb00d..755de14 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,22 @@ Build dynamic plugins from scratch — backend or frontend — and get them depl - **[export](./skills/create-plugin/references/export.md)** — Export, package (OCI/tgz/npm), and push to a container registry. - **[wiring](./skills/create-plugin/references/wiring.md)** — Analyze plugin source and generate `dynamic-plugins.yaml` wiring config. +### NFS Migration + +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. + +### Backstage Upgrade + +Upgrade `@backstage/*` dependencies in your plugin to align with a target RHDH or Backstage release. + +- **[backstage-upgrade](./skills/backstage-upgrade/SKILL.md)** -- Discovers current versions, determines the target using the RHDH→Backstage version matrix, runs `backstage-cli versions:bump`, migrates moved packages, guides through breaking changes from upstream changelogs, and verifies the result. Composable — the NFS migration skill chains into it automatically when deps are outdated. + ### Extensions Catalog Manage plugins in the [rhdh-plugin-export-overlays](https://github.com/redhat-developer/rhdh-plugin-export-overlays) repository. diff --git a/docs/nfs-migration-guide.md b/docs/nfs-migration-guide.md new file mode 100644 index 0000000..a519cd7 --- /dev/null +++ b/docs/nfs-migration-guide.md @@ -0,0 +1,484 @@ +# 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/pyproject.toml b/pyproject.toml index bad9a86..3d5653d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rhdh-skill" -version = "0.5.0" +version = "0.6.0" description = "Claude Code skill for RHDH plugin development" readme = "README.md" license = "Apache-2.0" diff --git a/skills/backstage-upgrade/SKILL.md b/skills/backstage-upgrade/SKILL.md new file mode 100644 index 0000000..8b1b86f --- /dev/null +++ b/skills/backstage-upgrade/SKILL.md @@ -0,0 +1,78 @@ +--- +name: backstage-upgrade +description: > + Upgrade @backstage/* dependencies in a plugin or app to a target version. + Use when asked to "upgrade backstage", "bump backstage", "update @backstage", + "align backstage deps", "backstage version bump", "upgrade dependencies", + "backstage-cli versions:bump", "update to latest backstage", "fix version + mismatch", "backstage version alignment", "upgrade before migration", + or any request to update Backstage package versions in a project. +--- + + + + +Always read the plugin's `package.json` (and `backstage.json` if present) before changing anything. Understand the current version baseline before upgrading. + + + +RHDH pins specific Backstage versions per release. If the plugin targets RHDH, use the version matrix from `../rhdh/references/versions.md` to determine the correct Backstage version. Don't blindly upgrade to the latest Backstage if it's ahead of what RHDH ships. **Note:** This path requires the `rhdh` core skill to be installed alongside. If the file is not found, ask the user for the target RHDH and Backstage versions directly. + + + +Use `backstage-cli versions:bump` for dependency upgrades instead of manually editing package.json. The CLI resolves the correct version for every `@backstage/*` package from the release manifest. + + + +This skill can be called standalone or chained from another skill (e.g., nfs-migration). When chained, the calling skill may pass additional breaking-change checklists. Apply those alongside the standard changelog review. + + + + + + +## What would you like to do? + +1. **Upgrade to latest Backstage for my RHDH version** — Align deps to the Backstage version that your target RHDH release uses +2. **Upgrade to a specific Backstage version** — Bump to an exact Backstage release (e.g., 1.45.3) +3. **Check what version I'm on** — Discover current `@backstage/*` versions without making changes +4. **Fix issues after a version bump** — Resolve breaking changes, moved packages, or build failures after upgrading + +**Wait for response before proceeding.** + + + + + +| Response | Action | +|----------|--------| +| 1, "latest", "RHDH", "align" | Follow `workflows/full-upgrade.md` (RHDH-aligned) | +| 2, "specific", "version", number like "1.45" | Follow `workflows/full-upgrade.md` (user-specified version) | +| 3, "check", "current", "what version" | Read `references/discover-versions.md` and report findings | +| 4, "fix", "breaking", "issues", "errors" | Read `references/fix-breaking-changes.md` and `references/migrate-packages.md` | + + + + + +| Reference | Load when... | +|-----------|-------------| +| `references/discover-versions.md` | Reading current Backstage versions from a project | +| `references/determine-target.md` | Figuring out what Backstage version to target | +| `references/bump-deps.md` | Running the version bump command | +| `references/migrate-packages.md` | Handling moved/renamed packages | +| `references/fix-breaking-changes.md` | Resolving breaking changes from changelogs | +| `references/verify-upgrade.md` | Verifying the upgrade succeeded | +| `../rhdh/references/versions.md` | Looking up RHDH → Backstage version mapping | + + + + + +- All `@backstage/*` deps align to the target release version +- No packages still reference old names (moved to `@backstage-community/*`) +- `yarn tsc` passes with no type errors +- `yarn build` succeeds +- `yarn test` passes (if tests exist) + + diff --git a/skills/backstage-upgrade/references/bump-deps.md b/skills/backstage-upgrade/references/bump-deps.md new file mode 100644 index 0000000..caa3b1c --- /dev/null +++ b/skills/backstage-upgrade/references/bump-deps.md @@ -0,0 +1,46 @@ +# Bump Backstage Dependencies + +## Primary command + +```bash +yarn backstage-cli versions:bump --release +``` + +Replace `` with the target Backstage release (e.g., `1.45.3`). + +This command: +- Reads the release manifest for the target version +- Updates all `@backstage/*` packages in `package.json` to their correct versions for that release +- Runs `yarn install` to update the lockfile +- Runs `versions:migrate` to handle moved packages (unless `--skipMigrate`) + +## Useful flags + +| Flag | Effect | +|------|--------| +| `--release ` | Target a specific release (default: `main`) | +| `--pattern '@{backstage,roadiehq}/*'` | Include additional package scopes | +| `--skipInstall` | Skip `yarn install` (useful if you want to review changes first) | +| `--skipMigrate` | Skip automatic migration of moved packages | + +## Workspace / monorepo usage + +In a monorepo (like `rhdh-plugins`), run from the workspace root. The command updates all packages across the workspace. + +If you only want to bump a single plugin package, you can run it from that package's directory -- but be aware that shared workspace deps may need alignment too. + +## What it doesn't do + +- Fix breaking API changes in your source code (see `fix-breaking-changes.md`) +- Update non-`@backstage/*` dependencies +- Migrate your code from legacy to NFS APIs + +## Troubleshooting + +**"Could not fetch release manifest"** — Check network connectivity. The CLI fetches from `https://versions.backstage.io`. You can also set `BACKSTAGE_MANIFEST_FILE` to a local file. + +**Lockfile conflicts** — Delete the lockfile and re-run `yarn install` after the bump. + +**Version not found** — Verify the release version exists. Check available releases at `https://github.com/backstage/backstage/releases`. + +**Workspace resolution errors** — In monorepos, ensure all workspace packages are using compatible version ranges. Run `yarn dedupe @backstage/*` after the bump. diff --git a/skills/backstage-upgrade/references/determine-target.md b/skills/backstage-upgrade/references/determine-target.md new file mode 100644 index 0000000..c8352b3 --- /dev/null +++ b/skills/backstage-upgrade/references/determine-target.md @@ -0,0 +1,38 @@ +# Determine Target Backstage Version + +## For RHDH plugins + +RHDH pins a specific Backstage version per release. Load the version matrix from `../../rhdh/references/versions.md` to find the mapping. + +> **Dependency:** RHDH version alignment requires the `rhdh` core skill to be installed alongside this skill. If `versions.md` is not found at that path, ask the user for the target RHDH and Backstage versions directly. + +Ask the user: **"Which RHDH version are you targeting?"** + +| RHDH Version | Backstage Version | +|---|---| +| See `../../rhdh/references/versions.md` for the current matrix | + +Use the Backstage version from the matrix as the `--release` argument for `versions:bump`. + +## For standalone Backstage projects + +If the plugin isn't targeting a specific RHDH release, ask the user: + +- **"Latest stable"** → Use the most recent Backstage release (check `https://versions.backstage.io` or the Backstage GitHub releases page) +- **Specific version** → Use what they specify (e.g., `1.45.3`) + +## Version format + +The `--release` flag for `backstage-cli versions:bump` accepts: + +- `main` — latest monthly release (default) +- `next` — latest weekly pre-release +- `1.45.3` — exact version pin + +For RHDH alignment, always use the exact version from the matrix. + +## Checking if an upgrade is needed + +Compare the current base version (from `discover-versions.md`) against the target. If they match, no upgrade is needed -- tell the user. + +If the target is older than current, warn the user -- downgrading is risky and may not be supported by `versions:bump`. diff --git a/skills/backstage-upgrade/references/discover-versions.md b/skills/backstage-upgrade/references/discover-versions.md new file mode 100644 index 0000000..98bca55 --- /dev/null +++ b/skills/backstage-upgrade/references/discover-versions.md @@ -0,0 +1,42 @@ +# Discover Current Backstage Versions + +## Read package.json + +Extract all `@backstage/*` dependencies from the plugin's `package.json`: + +```bash +cat package.json | grep '@backstage/' | sort +``` + +Check both `dependencies` and `devDependencies`. Note the versions -- they should all correspond to the same Backstage release. + +## Check backstage.json + +If the project root has a `backstage.json`, it tracks the overall Backstage version: + +```json +{ + "version": "1.45.3" +} +``` + +This is the canonical "base version" for the project. All `@backstage/*` packages should match this release. + +## Identify the base release + +`@backstage/*` packages are versioned independently, but each Backstage release pins a specific version for every package. To identify which release the current deps correspond to: + +1. Pick a core package like `@backstage/core-plugin-api` and note its version +2. Cross-reference against the [release manifests](https://versions.backstage.io) or the RHDH version matrix at `../../rhdh/references/versions.md` + +## Report to the user + +List: +- Current `backstage.json` version (if present) +- Current `@backstage/core-plugin-api` version (quick proxy for the release) +- Any `@backstage/*` packages that are out of sync (different release from the rest) +- Any `@backstage-community/*` packages (these were moved from `@backstage/*`) + +## Mixed versions + +If different `@backstage/*` packages are on different releases, this is a problem. The version bump will fix it, but flag it to the user -- mixed versions cause subtle runtime errors. diff --git a/skills/backstage-upgrade/references/fix-breaking-changes.md b/skills/backstage-upgrade/references/fix-breaking-changes.md new file mode 100644 index 0000000..64f3b12 --- /dev/null +++ b/skills/backstage-upgrade/references/fix-breaking-changes.md @@ -0,0 +1,57 @@ +# Fix Breaking Changes + +## Find relevant changelogs + +Identify the Backstage versions between your current and target release. For each version, check the changelog: + +- **Release notes:** `https://github.com/backstage/backstage/blob/master/docs/releases/v.md` +- **Detailed changelog:** `https://github.com/backstage/backstage/blob/master/docs/releases/v-changelog.md` + +Focus on the **"Breaking Changes"** and **"Minor Changes"** sections. Patch changes rarely require code updates. + +## Process + +For each breaking change in the changelogs: + +1. **Read the change description** — understand what was renamed, removed, or restructured +2. **Search the plugin's source** — `grep -r '' src/` to check if the plugin is affected +3. **Apply the fix** — follow the changelog's migration guidance +4. **Verify** — `yarn tsc` after each fix to confirm the type error is resolved + +## Common breaking change patterns + +| Pattern | What to look for | Fix | +|---------|-----------------|-----| +| Renamed export | `import { OldName }` → `import { NewName }` | Update import | +| Moved package | `@backstage/plugin-x` → `@backstage-community/plugin-x` | Run `versions:migrate` | +| Removed API | `import { removedFn }` | Replace with recommended alternative from changelog | +| Changed signature | Type errors on function calls | Update call site per changelog | +| New required field | Missing property errors | Add the new field | + +## Per-package changelogs + +Each `@backstage/*` package has its own `CHANGELOG.md` in the Backstage repo. If `yarn tsc` flags errors in a specific package, check: + +``` +https://github.com/backstage/backstage/blob/master/packages//CHANGELOG.md +https://github.com/backstage/backstage/blob/master/plugins//CHANGELOG.md +``` + +## Composability with other skills + +If you were directed here from another skill (e.g., `nfs-migration`), that skill may have its own breaking-change checklist. Apply both: + +1. The upstream Backstage changelogs (this file) +2. Any skill-specific checklist the calling skill referenced (e.g., `nfs-migration/references/api-changes.md`) + +The upstream changelogs cover all Backstage breaking changes. The skill-specific checklist covers domain-specific patterns (like NFS blueprint changes) that may not appear in the upstream changelog. + +## Backstage Upgrade Helper + +For visual diffs between Backstage versions, use the Upgrade Helper tool: + +``` +https://backstage.github.io/upgrade-helper/ +``` + +Select your current and target versions to see a diff of all template changes in `create-app`. diff --git a/skills/backstage-upgrade/references/migrate-packages.md b/skills/backstage-upgrade/references/migrate-packages.md new file mode 100644 index 0000000..d7cae23 --- /dev/null +++ b/skills/backstage-upgrade/references/migrate-packages.md @@ -0,0 +1,47 @@ +# Migrate Moved Packages + +## What are moved packages? + +Backstage has been moving community-maintained packages from the `@backstage/*` namespace to `@backstage-community/*`. When you upgrade, the old package names become deprecated and eventually removed. + +## Automatic migration + +```bash +yarn backstage-cli versions:migrate +``` + +This command: +1. Detects `@backstage/*` packages that have a `backstage.moved` field in their `package.json` pointing to the new name +2. Updates your `package.json` dependencies to use the new package name +3. Updates source code imports to use the new package path + +### Flags + +| Flag | Effect | +|------|--------| +| `--pattern '@backstage/*'` | Glob pattern for packages to check (default: `@backstage/*`) | +| `--skipCodeChanges` | Only update `package.json`, don't modify source imports | + +## Other migrate subcommands + +The `backstage-cli` has additional migration helpers: + +| Command | What it does | +|---------|-------------| +| `migrate package-roles` | Add missing `backstage.role` fields to `package.json` | +| `migrate package-scripts` | Align `scripts` in `package.json` to match the role | +| `migrate package-exports` | Synchronize `exports` field definitions | +| `migrate package-lint-configs` | Switch to `@backstage/cli/config/eslint-factory` | +| `migrate react-router-deps` | Move `react-router` deps to peer dependencies | + +Run these when the type checker or build flags issues related to package configuration. + +## Manual check + +After running `versions:migrate`, grep for any remaining old-namespace imports: + +```bash +grep -r '@backstage/plugin-' src/ --include='*.ts' --include='*.tsx' | grep -v node_modules +``` + +Cross-reference any hits with the [Backstage community plugins repo](https://github.com/backstage/community-plugins) to check if they've been moved. diff --git a/skills/backstage-upgrade/references/verify-upgrade.md b/skills/backstage-upgrade/references/verify-upgrade.md new file mode 100644 index 0000000..274b650 --- /dev/null +++ b/skills/backstage-upgrade/references/verify-upgrade.md @@ -0,0 +1,58 @@ +# Verify Upgrade + +Run these checks in order. Stop and fix any failures before continuing. + +## Build checks + +```bash +# Type check +yarn tsc + +# Build +yarn build + +# Lint (if configured) +yarn lint +``` + +## Test suite + +```bash +yarn test +``` + +If tests fail, check whether the failures are due to: +- API changes (fix per `fix-breaking-changes.md`) +- Snapshot mismatches (update snapshots: `yarn test -u`) +- Moved packages (run `versions:migrate`) + +## Import validation + +Check for deprecated or moved package imports: + +```bash +# Packages moved to @backstage-community +grep -r '@backstage/plugin-' src/ --include='*.ts' --include='*.tsx' | grep -v node_modules | head -20 +``` + +Cross-reference any hits against the community plugins repo to verify they haven't been moved. + +## Version consistency + +Verify all `@backstage/*` packages are on the same release: + +```bash +cat package.json | grep '@backstage/' | sort +``` + +All versions should correspond to the target release. If any are out of sync, re-run `versions:bump`. + +## Runtime check (if a dev app exists) + +```bash +yarn start +``` + +- Open the browser and verify the plugin loads +- Check the browser console for errors +- Verify core functionality works diff --git a/skills/backstage-upgrade/workflows/full-upgrade.md b/skills/backstage-upgrade/workflows/full-upgrade.md new file mode 100644 index 0000000..84c4b4d --- /dev/null +++ b/skills/backstage-upgrade/workflows/full-upgrade.md @@ -0,0 +1,80 @@ +# Full Backstage Upgrade + + +- Plugin or app with `@backstage/*` dependencies +- `yarn` or `npm` available +- `@backstage/cli` installed (or available via `npx`) +- Network access to fetch release manifests + + + + +## Phase 1: Discover + +Load `references/discover-versions.md` and identify: +- Current `@backstage/*` versions +- Base Backstage release version +- Any version misalignment across packages + +Report findings to the user before proceeding. + +## Phase 2: Determine Target + +Load `references/determine-target.md`. + +- If the user chose **"latest for my RHDH version"**: load `../../rhdh/references/versions.md`, ask which RHDH version they target, and use the corresponding Backstage version. +- If the user chose **"specific version"**: use the version they provided. + +Compare current vs target. If they match, report "Already on target version" and stop. + +## Phase 3: Bump Dependencies + +Load `references/bump-deps.md` and run: + +```bash +yarn backstage-cli versions:bump --release +``` + +Review the changes to `package.json` before continuing. + +## Phase 4: Migrate Moved Packages + +Load `references/migrate-packages.md` and run: + +```bash +yarn backstage-cli versions:migrate +``` + +Check for any remaining old-namespace imports. + +## Phase 5: Fix Breaking Changes + +Load `references/fix-breaking-changes.md`. + +1. Identify all Backstage releases between the old and new version +2. Read the changelogs for breaking changes +3. Search the plugin source for affected APIs +4. Apply fixes + +If you were directed here from another skill, also apply any breaking-change checklist they referenced. + +## Phase 6: Verify + +Load `references/verify-upgrade.md` and run all checks: + +```bash +yarn tsc +yarn build +yarn test +``` + +Fix any failures before reporting success. + + + + +- All `@backstage/*` deps match the target release +- No deprecated or moved package imports remain +- `yarn tsc`, `yarn build`, and `yarn test` pass +- No console errors when running the dev app (if applicable) + diff --git a/skills/nfs-migration/SKILL.md b/skills/nfs-migration/SKILL.md new file mode 100644 index 0000000..5428a23 --- /dev/null +++ b/skills/nfs-migration/SKILL.md @@ -0,0 +1,154 @@ +--- +name: nfs-migration +description: > + Migrate Backstage frontend plugins from the legacy system to the New Frontend + System (NFS). Use when asked to "migrate to NFS", "new frontend system", + "convert plugin to NFS", "createFrontendPlugin", "PageBlueprint", + "ApiBlueprint", "SubPageBlueprint", "alpha to GA", "legacy to NFS", + "frontend migration", "extension blueprints", "migrate frontend plugin", + "NFS support", "graduate alpha", or mentions migrating a Backstage plugin + 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. + + + +Use `@backstage/frontend-plugin-api` for core blueprints. RHDH-specific blueprints (`AppDrawerContentBlueprint`, `GlobalHeaderMenuItemBlueprint`) come from `@red-hat-developer-hub/*` packages. Don't mix them up. + + + +Entity content and cards can go directly in the plugin's `extensions` array — the blueprint declares its own attach point. Use `createFrontendModule` only for extensions that target a different plugin (translations → `pluginId: 'app'`, homepage widgets → `pluginId: 'home'`) or when injecting content from outside a plugin you don't own. + + + +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. + + + +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. + + + + + + +## What would you like to do? + +1. **Migrate a plugin to NFS** — Analyze your existing plugin and convert it to the New Frontend System +2. **Test a migrated plugin in RHDH** — Deploy and verify in a local or cluster RHDH instance +3. **Learn about NFS migration** — Read the migration guide + +**Wait for response before proceeding.** + + + + + +| Response | Action | +|----------|--------| +| 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 | + + + + + +### Step 1: Discover + +Read `package.json` and `src/plugin.ts` (or `src/plugin.tsx`). Identify: +- Plugin ID +- Routes and route refs +- API factories +- Routable extensions (pages) +- Component extensions (entity cards, tabs) +- Sidebar/nav items +- Translations +- RHDH-specific extensions (drawers, header items, homepage widgets) +- RHDH dynamic plugin mount points (`app-config.dynamic.yaml` — see `references/mount-point-mapping.md`) + +List all findings to the user before proceeding. + +If the plugin's `@backstage/*` dependencies are outdated, upgrade them first using the `backstage-upgrade` skill (`../backstage-upgrade/SKILL.md`) before proceeding with migration. + +### Step 2: Choose Approach + +Use **Direct to GA** by default: NFS becomes root export (`.`), legacy at `./legacy`. + +Only ask about the **Phased** approach (`./alpha`) if the user says they have external consumers that can't migrate yet. + +### Step 3: Migrate Extensions + +For each extension type found in Step 1, load the appropriate reference: + +| Extension type | Reference to load | +|----------------|-------------------| +| Pages, API factories | `references/migrate-page.md` | +| Entity content tabs or cards | `references/migrate-entity-content.md` | +| Translations / i18n | `references/migrate-translations.md` | +| RHDH drawers, header items, homepage widgets | `references/migrate-rhdh-extensions.md` | +| App-level wrappers or root elements | `references/migrate-app-level.md` | + +Apply each reference's patterns to the discovered extensions. For page plugins, create NFS variants of page components without the page shell (dual header pattern in `migrate-page.md`). + +### Step 4: Update package.json + +Load `references/package-json.md` and apply the export configuration matching the chosen approach (GA or phased). + +### 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) + +### Step 6: Verify + +Load `references/verification.md` and run all checks. Run `yarn tsc` from the **workspace root** (not just the plugin directory) to catch consumer import issues. + + + + + +| Reference | Load when... | +|-----------|-------------| +| `references/migrate-page.md` | Plugin has pages or API factories | +| `references/api-changes.md` | Updating a plugin migrated against an older NFS version | +| `references/migrate-entity-content.md` | Plugin has entity tabs or cards | +| `references/migrate-translations.md` | Plugin has i18n/translations | +| `references/migrate-rhdh-extensions.md` | Plugin uses RHDH drawer, header, or homepage widgets | +| `references/mount-point-mapping.md` | Plugin uses RHDH dynamic plugin mount points (legacy config) | +| `references/migrate-app-level.md` | Plugin has app-level wrappers or root elements | +| `references/package-json.md` | Updating package.json exports | +| `references/app-setup.md` | Setting up or updating the NFS dev app | +| `references/verification.md` | Verifying the migration | +| `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/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 +- 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) +- 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) + + diff --git a/skills/nfs-migration/examples/before-after-drawer.md b/skills/nfs-migration/examples/before-after-drawer.md new file mode 100644 index 0000000..9d3ccad --- /dev/null +++ b/skills/nfs-migration/examples/before-after-drawer.md @@ -0,0 +1,54 @@ +# Before/After: Drawer Plugin Migration (RHDH-Specific) + +Drawer plugin migration based on the lightspeed/quickstart pattern. + +## Before (Legacy) + +```tsx +// src/plugin.ts +export const MyDrawerContent = myPlugin.provide( + createComponentExtension({ + name: 'MyDrawerContent', + component: { lazy: () => import('./components/DrawerContent').then(m => m.DrawerContent) }, + }), +); +// Wired via app-config.dynamic.yaml mount points +``` + +## After (NFS) + +### Drawer content plugin + +```tsx +import { createFrontendPlugin, createFrontendModule, AppRootElementBlueprint } from '@backstage/frontend-plugin-api'; +import { AppDrawerContentBlueprint } from '@red-hat-developer-hub/backstage-plugin-app-react/alpha'; +import { MY_DRAWER_ID } from './const'; + +const myDrawer = AppDrawerContentBlueprint.make({ + name: 'my-drawer', + params: { + id: MY_DRAWER_ID, + loader: () => import('./components/DrawerContent').then(m => ), + resizable: true, + defaultWidth: 400, + }, +}); + +export default createFrontendPlugin({ + pluginId: 'my-drawer-plugin', + extensions: [myDrawer], +}); + +// Init logic (auto-open, snackbar) goes in a separate app module +const myInitElement = AppRootElementBlueprint.make({ + name: 'my-drawer-init', + params: { element: }, +}); + +export const myDrawerInitModule = createFrontendModule({ + pluginId: 'app', + extensions: [myInitElement], +}); +``` + +> **Note:** Drawer content only renders when the drawer is active. Init logic that should run on page load (e.g. auto-open triggers, snackbar notifications) needs `AppRootElementBlueprint` in a separate module attached to the `app` plugin. diff --git a/skills/nfs-migration/examples/before-after-entity.md b/skills/nfs-migration/examples/before-after-entity.md new file mode 100644 index 0000000..1ceaa77 --- /dev/null +++ b/skills/nfs-migration/examples/before-after-entity.md @@ -0,0 +1,70 @@ +# Before/After: Entity Tab Migration + +Complete entity tab migration based on the scorecard/orchestrator pattern. + +## Before (Legacy) + +```tsx +// In the app's EntityPage.tsx +import { EntityScorecardContent } from '@scope/my-plugin'; + + + + +``` + +## After (NFS) + +### Basic entity content extension + +```tsx +// src/alpha/entityTab.tsx (or inline in alpha/index.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', + filter: 'kind:Component', + loader: () => import('./components/MyTab').then(m => ), + }, +}); + +export default createFrontendPlugin({ + pluginId: 'my-plugin', + extensions: [myEntityContent], +}); +``` + +### Config-driven filtering variant + +Use `EntityContentBlueprint.makeWithOverrides` when you want operators to control filtering via `app-config.yaml` instead of hardcoding it: + +```tsx +import { z } from 'zod/v4'; + +const myEntityContent = EntityContentBlueprint.makeWithOverrides({ + configSchema: { + filter: z.string().optional(), + }, + factory(originalFactory, { config }) { + return originalFactory({ + path: '/my-tab', + title: 'My Tab', + filter: config.filter ?? 'kind:Component', + loader: () => import('./components/MyTab').then(m => ), + }); + }, +}); +``` + +This lets operators override the entity filter in `app-config.yaml`: + +```yaml +app: + extensions: + - entity-content:catalog/my-tab: + config: + filter: 'kind:Component,API' +``` diff --git a/skills/nfs-migration/examples/before-after-page.md b/skills/nfs-migration/examples/before-after-page.md new file mode 100644 index 0000000..fa6cbdc --- /dev/null +++ b/skills/nfs-migration/examples/before-after-page.md @@ -0,0 +1,95 @@ +# Before/After: Page Plugin Migration + +Complete page plugin migration based on the adoption-insights pattern. + +## Before (Legacy) + +### Plugin definition + +```typescript +// src/plugin.ts +import { createPlugin, createApiFactory, createRoutableExtension, 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 }), + }), + ], +}); + +export const MyPluginPage = myPlugin.provide( + createRoutableExtension({ + name: 'MyPluginPage', + component: () => import('./components/MyPage').then(m => m.MyPage), + mountPoint: rootRouteRef, + }), +); +``` + +### App wiring (legacy) + +```tsx +// App.tsx +import { MyPluginPage } from '@scope/my-plugin'; +} /> +``` + +## After (NFS) + +### Plugin definition + +```tsx +// src/index.ts (default export) +import { createFrontendPlugin, ApiBlueprint, PageBlueprint, configApiRef, fetchApiRef, createApiFactory } from '@backstage/frontend-plugin-api'; +import { rootRouteRef } from './routes'; +import { myApiRef, MyApiClient } from './api'; +import MyIcon from '@mui/icons-material/Extension'; + +const myApi = ApiBlueprint.make({ + params: defineParams => defineParams( + createApiFactory({ + 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: MyIcon, + routeRef: rootRouteRef, + loader: () => import('./components/MyPage').then(m => ), + }, +}); + +export default createFrontendPlugin({ + pluginId: 'my-plugin', + title: 'My Plugin', + icon: MyIcon, + extensions: [myApi, myPage], + routes: { root: rootRouteRef }, +}); +``` + +> **Nav items:** The legacy `SidebarItem` is replaced by auto-discovery — `title` + `icon` + `routeRef` on the page generates a nav entry automatically. + +> **Dual header:** The loader imports `NfsMyPage` (not `MyPage`) — the NFS variant without the page shell. Create it by extracting the content from `MyPage` without the `PageWithHeader` wrapper. See `references/migrate-page.md` for the full pattern. + +### App wiring (NFS) + +```tsx +// App.tsx +import { createApp } from '@backstage/frontend-defaults'; +import myPlugin from '@scope/my-plugin'; +export default createApp({ features: [myPlugin] }); +``` diff --git a/skills/nfs-migration/references/api-changes.md b/skills/nfs-migration/references/api-changes.md new file mode 100644 index 0000000..0f01f98 --- /dev/null +++ b/skills/nfs-migration/references/api-changes.md @@ -0,0 +1,202 @@ +# NFS API Changes + +Breaking and notable changes between the early NFS alpha and the current Backstage GA codebase. Reference this when upgrading Backstage versions or updating plugins that were migrated against an older NFS API. + +> To upgrade your `@backstage/*` dependencies to the version that includes these changes, use the `backstage-upgrade` skill (`../backstage-upgrade/SKILL.md`). + +## 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. + +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. + +`compatWrapper()` is only needed when a component depends on legacy context providers (e.g. old `SidebarContext`) that aren't available in NFS. Most plugins won't need it. + +## NavItemBlueprint removed + +`NavItemBlueprint` from `@backstage/frontend-plugin-api` has been removed. Nav items are now **auto-discovered** from `PageBlueprint` extensions. + +When a page has `routeRef`, `title`, and `icon`, the app nav system automatically generates a sidebar entry. No separate blueprint is needed. + +**If you previously used NavItemBlueprint:** + +```tsx +// Old — remove this +const myNavItem = NavItemBlueprint.make({ + params: { title: 'My Plugin', routeRef: rootRouteRef, icon: MyIcon }, +}); + +// New — add title and icon to PageBlueprint and/or createFrontendPlugin +const myPage = PageBlueprint.make({ + params: { + path: '/my-plugin', + title: 'My Plugin', + icon: MyIcon, + routeRef: rootRouteRef, + loader: () => import('./components/MyPage').then(m => ), + }, +}); + +export default createFrontendPlugin({ + pluginId: 'my-plugin', + title: 'My Plugin', + icon: MyIcon, + extensions: [myApi, myPage], + routes: { root: rootRouteRef }, +}); +``` + +**Priority chain** for auto-discovered nav items: +- Title: explicit `PageBlueprint` title > `createFrontendPlugin` title > pluginId +- Icon: explicit `PageBlueprint` icon > `createFrontendPlugin` icon > (excluded from nav) + +If you need a standalone nav entry without a page, or full control over the nav bar, use `NavContentBlueprint` from `@backstage/plugin-app-react` to replace the entire nav component. + +## `makeWithOverrides` config pattern deprecated + +The `config: { schema: {...} }` pattern is deprecated. Use top-level `configSchema` instead. + +```tsx +// Old (deprecated) +EntityContentBlueprint.makeWithOverrides({ + config: { + schema: { + filter: z => z.string().optional(), + }, + }, + factory(originalFactory, { config }) { ... }, +}); + +// New +import { z } from 'zod/v4'; + +EntityContentBlueprint.makeWithOverrides({ + configSchema: { + filter: z.string().optional(), + }, + factory(originalFactory, { config }) { ... }, +}); +``` + +Note: the `z` import changes from the callback form `z => z.string()` to a direct import from `zod/v4`. + +## `AppRootWrapperBlueprint` param rename + +The `Component` param (uppercase) is deprecated. Use `component` (lowercase). + +Import from `@backstage/plugin-app-react`, **not** `@backstage/frontend-plugin-api`. + +```tsx +// Old +AppRootWrapperBlueprint.make({ params: { Component: MyWrapper } }); + +// New +import { AppRootWrapperBlueprint } from '@backstage/plugin-app-react'; +AppRootWrapperBlueprint.make({ params: { component: MyWrapper } }); +``` + +## Deprecated param names in blueprints + +Several blueprint params were renamed. The old names produce TypeScript errors: + +| Blueprint | Old param (deprecated) | New param | +|-----------|----------------------|-----------| +| `PageBlueprint` | `defaultPath` | `path` | +| `EntityContentBlueprint` | `defaultPath` | `path` | +| `EntityContentBlueprint` | `defaultTitle` | `title` | +| `EntityContentBlueprint` | `defaultGroup` | `group` | + +## `createFrontendPlugin` now accepts `title` and `icon` + +These are used as fallbacks for page headers and auto-discovered nav entries: + +```tsx +export default createFrontendPlugin({ + pluginId: 'my-plugin', + title: 'My Plugin', + icon: MyIcon, + extensions: [myPage, myApi], + routes: { root: rootRouteRef }, +}); +``` + +## `PageBlueprint` new params + +- `title?: string` — page header title, also used for nav item auto-discovery +- `icon?: IconElement` — page header icon, also used for nav item +- `noHeader?: boolean` — hides the default plugin page header for full-bleed layouts + +## `SubPageBlueprint` added + +Creates tabbed sub-pages within a parent `PageBlueprint`. Exported from `@backstage/frontend-plugin-api`. + +```tsx +import { SubPageBlueprint } from '@backstage/frontend-plugin-api'; + +const overviewPage = SubPageBlueprint.make({ + attachTo: { id: 'page:my-plugin', input: 'pages' }, + name: 'overview', + params: { + path: 'overview', + title: 'Overview', + loader: () => import('./components/Overview').then(m => ), + }, +}); +``` + +Note: the `path` must NOT start with `/` (it's relative to the parent page). + +## NFS page components — no page shell + +NFS pages must not include `PageWithHeader`, `Page` + `Header`, or any page shell component. The framework provides the header automatically via `PageLayout`. Create NFS-specific page variants: + +```tsx +// Legacy — includes page shell +export function MyPage() { + return ( + + + + ); +} + +// NFS — content only +export function NfsMyPage() { + return ; +} +``` + +Load the NFS variant in `PageBlueprint`: +```tsx +loader: () => import('./components/MyPage').then(m => ) +``` + +## `useRouteRef` returns `undefined` in NFS + +`useRouteRef` from `@backstage/frontend-plugin-api` returns `RouteFunc | undefined`. The legacy version from `core-plugin-api` throws on unbound routes. Handle the `undefined` case in NFS components. + +## Remix Icons preferred + +Use [Remix Icons](https://remixicon.com/) from `@remixicon/react` for plugin icons. MUI icons work with `fontSize="inherit"` but Remix is the recommended choice for new plugins. + +## External route refs — `defaultTarget` + +Set `defaultTarget` on external route refs so plugins work out-of-the-box without `bindRoutes`: + +```tsx +export const viewTechDocRouteRef = createExternalRouteRef({ + id: 'view-techdoc', + optional: true, + defaultTarget: 'techdocs.docRoot', +}); +``` + +## New catalog-react blueprints + +The `@backstage/plugin-catalog-react/alpha` package now exports additional blueprints: + +- `CatalogFilterBlueprint` — custom catalog filters +- `EntityContentLayoutBlueprint` — entity content layouts +- `EntityContextMenuItemBlueprint` — entity context menu items +- `EntityHeaderBlueprint` — custom entity headers +- `EntityIconLinkBlueprint` — entity icon links diff --git a/skills/nfs-migration/references/app-setup.md b/skills/nfs-migration/references/app-setup.md new file mode 100644 index 0000000..9c3b727 --- /dev/null +++ b/skills/nfs-migration/references/app-setup.md @@ -0,0 +1,113 @@ +# NFS App Setup + +## App entry point + +```tsx +import { createApp } from '@backstage/frontend-defaults'; +import catalogPlugin from '@backstage/plugin-catalog/alpha'; +import myPlugin, { myTranslationsModule, myCatalogModule } from '@scope/my-plugin'; + +const app = createApp({ + features: [ + catalogPlugin, + myPlugin, + myTranslationsModule, + myCatalogModule, + ], +}); + +export default app; +``` + +## index.tsx + +```tsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render(App.createRoot()); +``` + +## Import rules + +| Approach | Plugin import | Module imports | +|----------|--------------|----------------| +| Direct to GA | `import myPlugin from '@scope/my-plugin'` | `import { myTranslationsModule } from '@scope/my-plugin'` | +| Phased | `import myPlugin from '@scope/my-plugin/alpha'` | `import { myTranslationsModule } from '@scope/my-plugin/alpha'` | + +- The default export is always the plugin (`createFrontendPlugin` result) +- Named exports are modules (`createFrontendModule` results) +- Each module must be listed individually in `features` + +## 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. + +### NFS dev app (`dev/index.tsx`) + +Use `createApp` from `@backstage/frontend-defaults` with `createFrontendModule` for mock APIs: + +```tsx +import ReactDOM from 'react-dom/client'; +import { createApp } from '@backstage/frontend-defaults'; +import { ApiBlueprint, createFrontendModule } from '@backstage/frontend-plugin-api'; +import catalogPlugin from '@backstage/plugin-catalog/alpha'; +import myPlugin from '../src/plugin'; + +const myDevModule = createFrontendModule({ + pluginId: 'my-plugin', + extensions: [ + ApiBlueprint.make({ + name: 'my-api-mock', + params: defineParams => defineParams({ + api: myApiRef, + deps: {}, + factory: () => new MockApiClient(), + }), + }), + ], +}); + +const app = createApp({ + features: [catalogPlugin, myPlugin, myDevModule], +}); + +ReactDOM.createRoot(document.getElementById('root')!).render(app.createRoot()); +``` + +To redirect `/` to a default page, add to `app-config.yaml`: + +```yaml +app: + extensions: + - app/routes: + config: + redirects: + - from: / + to: /catalog +``` + +### Legacy dev app (`dev/legacy.tsx`) + +Move the old `createDevApp` from `@backstage/dev-utils` code here. Add to `package.json`: + +```json +"start:legacy": "backstage-cli package start --entrypoint dev/legacy" +``` + +## Consumer migration (packages/app) + +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: + +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`. + +## Dynamic plugin considerations (RHDH) + +When running as a dynamic plugin in RHDH: +- The app loads plugins automatically from `dynamic-plugins.yaml` +- No manual `features` array needed — RHDH handles registration +- Ensure the plugin's `package.json` has correct `backstage.role` and `pluginId` +- Modules must be exported and declared in the dynamic plugin config +- Test with `APP_CONFIG_app_packageName=app-next` and `ENABLE_STANDARD_MODULE_FEDERATION=true` to use NFS app diff --git a/skills/nfs-migration/references/gotchas.md b/skills/nfs-migration/references/gotchas.md new file mode 100644 index 0000000..8bb8fbd --- /dev/null +++ b/skills/nfs-migration/references/gotchas.md @@ -0,0 +1,141 @@ +# Common Migration Gotchas + +## 1. TranslationBlueprint in the wrong module + +**Why:** Translations must target `pluginId: 'app'` because they're app-level resources, not plugin-scoped. + +**Fix:** Move `TranslationBlueprint` into `createFrontendModule({ pluginId: 'app', extensions: [...] })`. Export the module separately. + +## 2. Missing nav item for a page + +**Why:** Nav items are auto-discovered from `PageBlueprint` extensions. If `title`, `icon`, or `routeRef` is missing, the page won't appear in the sidebar. + +**Fix:** Set all three on `PageBlueprint.make()` params. You can also set `title` and `icon` on `createFrontendPlugin` as fallbacks. + +> **Version note:** `NavItemBlueprint` was removed in recent Backstage versions. If upgrading, delete the `.make()` call and move `title`/`icon` into your `PageBlueprint` params. See `api-changes.md`. + +## 3. Entity content not discovered on entity pages + +**Why:** `EntityContentBlueprint` requires `path`, `title`, and `loader` to render. If any are missing, the tab won't appear. The blueprint declares its own attach point, so it works in the plugin's `extensions` array — no separate catalog module needed. + +**Fix:** Verify all required params are set. If providing entity content from a separate package (third-party addon), use `createFrontendModule({ pluginId: 'catalog', extensions: [...] })` instead. + +## 4. Missing `createApiFactory` wrapper in ApiBlueprint + +**Why:** `ApiBlueprint.make` expects a `defineParams` callback wrapping a `createApiFactory(...)` call. Passing raw config or a plain object won't work. + +**Fix:** +```tsx +// Wrong +ApiBlueprint.make({ params: { api: myApiRef, deps: {...}, factory: (...) => ... } }) + +// Right +ApiBlueprint.make({ + params: defineParams => defineParams( + createApiFactory({ api: myApiRef, deps: {...}, factory: (...) => ... }) + ), +}) +``` + +## 5. Using API refs from `@backstage/core-plugin-api` instead of `@backstage/frontend-plugin-api` + +**Why:** NFS has its own API refs. The old `core-plugin-api` refs don't resolve in the new system. (Route refs from `core-plugin-api` are fine — only API refs like `configApiRef`, `fetchApiRef`, etc. need migrating.) + +**Fix:** Replace all imports: +- `configApiRef` → from `@backstage/frontend-plugin-api` +- `fetchApiRef` → from `@backstage/frontend-plugin-api` +- `identityApiRef` → from `@backstage/frontend-plugin-api` +- Same for `discoveryApiRef`, `errorApiRef`, `analyticsApiRef`, etc. + +## 6. Legacy components failing after migration + +**Why:** Components may depend on legacy context providers (e.g. old `SidebarContext`) that aren't available in NFS. + +**What works without changes:** Hooks like `useApi` and `useRouteRef` from `@backstage/core-plugin-api` work in both legacy and NFS. Keep component imports on `core-plugin-api` so the same components serve both export paths. + +**Fix (when needed):** If a component depends on a legacy context provider that isn't available in NFS, wrap it with `compatWrapper()` from `@backstage/core-compat-api`: +```tsx +loader: () => import('./MyComponent').then(m => compatWrapper()) +``` + +Most plugins won't need `compatWrapper` — it's rare. + +## 7. Drawer content with init logic + +**Why:** Drawer components mount/unmount with the drawer. Init logic (event listeners, global state) gets torn down when the drawer closes. + +**Fix:** Extract init logic into a separate `AppRootElementBlueprint`. Keep the drawer component purely presentational. + +## 8. Module federation singleton issues + +**Why:** When running as a dynamic plugin, multiple copies of `@backstage/plugin-app-react` or `@backstage/frontend-plugin-api` cause context mismatches. + +**Fix:** Ensure these packages are shared as singletons in webpack/module federation config: +```js +shared: { + '@backstage/plugin-app-react': { singleton: true }, + '@backstage/frontend-plugin-api': { singleton: true }, +} +``` + +## 9. Forgetting to update package.json exports/typesVersions + +**Why:** Without proper `exports` and `typesVersions`, consumers can't import from sub-paths (`./alpha`, `./legacy`). + +**Fix:** See `package-json.md` for the complete configuration. Both `exports` and `typesVersions` must be updated together. + +## 10. Forgetting the plugin must be the default export + +**Why:** NFS apps discover plugins via default imports. A named export won't be picked up by the app's `features` array or dynamic plugin loading. + +**Fix:** +```tsx +// Wrong — named export +export const myPlugin = createFrontendPlugin({ pluginId: 'my-plugin', ... }); + +// Right — default export +export default createFrontendPlugin({ pluginId: 'my-plugin', ... }); +``` + +## 11. JSX in a `.ts` file + +**Why:** Blueprint loaders return JSX (e.g., ``). TypeScript doesn't parse JSX in `.ts` files. + +**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` + +**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. + +**Fix:** Legacy exports should only be reachable via the `./legacy` subpath: +```tsx +// src/index.ts — NFS only +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'; +``` + +## 13. Double headers in NFS pages + +**Why:** Legacy page components include `PageWithHeader` or `Page` + `Header`. In NFS, the framework provides the header via `PageLayout` — using both produces double headers. + +**Fix:** Create an NFS variant of each page component without the page shell. Load the NFS variant in `PageBlueprint`: +```tsx +loader: () => import('./components/MyPage').then(m => ) +``` + +See `migrate-page.md` for the dual header pattern. + +## 14. `useRouteRef` returns `undefined` in NFS + +**Why:** `useRouteRef` from `@backstage/frontend-plugin-api` returns `RouteFunc | undefined`. The legacy version from `core-plugin-api` throws on unbound routes instead. + +**Fix:** When writing NFS-specific components, handle the `undefined` case: +```tsx +const link = useRouteRef(myRouteRef); +// link might be undefined — check before calling +const href = link?.() ?? '/fallback'; +``` diff --git a/skills/nfs-migration/references/migrate-app-level.md b/skills/nfs-migration/references/migrate-app-level.md new file mode 100644 index 0000000..cf82039 --- /dev/null +++ b/skills/nfs-migration/references/migrate-app-level.md @@ -0,0 +1,66 @@ +# App-Level Extension Migration + +## AppRootWrapperBlueprint — wraps the entire app + +Use for providers, theme wrappers, or any component that needs to wrap the whole React tree. + +```tsx +import { AppRootWrapperBlueprint } from '@backstage/plugin-app-react'; + +const appWrapper = AppRootWrapperBlueprint.make({ + name: 'my-provider', + params: { + component: ({ children }) => ( + + {children} + + ), + }, +}); +``` + +## AppRootElementBlueprint — invisible root elements + +Use for initialization logic, snackbar containers, FAB buttons, or anything that renders at the app root without wrapping children. + +```tsx +import { AppRootElementBlueprint } from '@backstage/frontend-plugin-api'; + +const appElement = AppRootElementBlueprint.make({ + name: 'my-init', + params: { + element: , + }, +}); +``` + +## 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: + +```tsx +// Keep this — works in both legacy and NFS +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +``` + +## compatWrapper — rare + +Only needed when a component depends on legacy context providers (e.g., `SidebarContext`) that aren't available in NFS. Wrap the JSX element in the loader: + +```tsx +loader: () => import('./components/MyPage').then(m => compatWrapper()) +``` + +Import `compatWrapper` from `@backstage/core-compat-api`. Most plugins won't need this. + +## When to use each + +| Scenario | Approach | +|----------|----------| +| Need to wrap entire app (providers, themes) | `AppRootWrapperBlueprint` | +| 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) | +| 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 new file mode 100644 index 0000000..ffda6c6 --- /dev/null +++ b/skills/nfs-migration/references/migrate-entity-content.md @@ -0,0 +1,89 @@ +# Entity Content and Card Migration + +Entity content and cards can go directly in your plugin's `extensions` array. The `EntityContentBlueprint` declares its own attach point (`page:catalog/entity`), so the app discovers them automatically regardless of where they're registered. + +Use `createFrontendModule({ pluginId: 'catalog' })` only when you're injecting entity content from a separate package that doesn't own the plugin (e.g., a third-party addon). + +## EntityContentBlueprint — replaces entity tab routes + +```tsx +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; + +const entityContent = EntityContentBlueprint.make({ + name: 'my-tab', + params: { + path: '/my-plugin', + title: 'My Plugin', + loader: () => import('./components/MyEntityPage').then(m => ), + }, +}); +``` + +### With config-driven entity filtering + +Use `makeWithOverrides` to support `filter` from app-config: + +```tsx +import { z } from 'zod/v4'; + +const entityContent = EntityContentBlueprint.makeWithOverrides({ + name: 'my-tab', + configSchema: { + filter: z.string().optional(), + }, + factory(originalFactory, { config }) { + return originalFactory({ + path: '/my-plugin', + title: 'My Plugin', + filter: config.filter || 'kind:component', + loader: () => import('./components/MyEntityPage').then(m => ), + }); + }, +}); +``` + +> **Version note:** Earlier versions used `config: { schema: { filter: z => z.string().optional() } }`. This is deprecated -- use top-level `configSchema` with direct `zod/v4` imports instead. See `api-changes.md`. + +## EntityCardBlueprint — replaces entity overview cards + +Same pattern as `EntityContentBlueprint` but for cards displayed on entity overview pages: + +```tsx +import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha'; + +const entityCard = EntityCardBlueprint.make({ + name: 'my-card', + params: { + filter: 'kind:component', + loader: () => import('./components/MyCard').then(m => ), + }, +}); +``` + +## Register in your plugin + +Include entity extensions in your plugin's `extensions` array: + +```tsx +import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; + +export default createFrontendPlugin({ + pluginId: 'my-plugin', + extensions: [entityContent, entityCard], +}); +``` + +### Alternative: separate module + +If you're providing entity content from a package that doesn't own the plugin (e.g., a third-party addon), use a module instead: + +```tsx +import { createFrontendModule } from '@backstage/frontend-plugin-api'; + +export const myCatalogModule = createFrontendModule({ + pluginId: 'catalog', + extensions: [entityContent, entityCard], +}); +``` + +Export the module so consumers can include it in their app's `features` array. diff --git a/skills/nfs-migration/references/migrate-page.md b/skills/nfs-migration/references/migrate-page.md new file mode 100644 index 0000000..1b834e3 --- /dev/null +++ b/skills/nfs-migration/references/migrate-page.md @@ -0,0 +1,240 @@ +# Page and API Migration + +## PageBlueprint — replaces `createRoutableExtension` + +```tsx +import { PageBlueprint } from '@backstage/frontend-plugin-api'; + +const myPage = PageBlueprint.make({ + params: { + path: '/my-plugin', + title: 'My Plugin', + icon: MyIcon, + routeRef: rootRouteRef, + loader: () => import('./components/MyPage').then(m => ), + }, +}); +``` + +- `path`: URL path for this page +- `title`: page header title; also used for auto-discovered nav items +- `icon`: page header icon; also used for nav items. Prefer [Remix Icons](https://remixicon.com/) (`@remixicon/react`). MUI icons work with `fontSize="inherit"` +- `routeRef`: route ref created with `createRouteRef` +- `loader`: async factory returning a JSX element +- `noHeader`: (optional) hides the default plugin page header + +Nav items are auto-generated from pages with `title` + `icon` + `routeRef` — no separate blueprint needed. You can also set `title` and `icon` on `createFrontendPlugin` as fallbacks. + +> Earlier versions used `NavItemBlueprint`. It has been removed — see `api-changes.md`. + +## Dual header pattern + +In the old system, each page renders its own page shell (`PageWithHeader`, `Page` + `Header`). In NFS, the framework provides the page header automatically via `PageLayout` — so NFS page components must **not** include their own page shell. Without this, you get **double headers**. + +### Pattern A: Separate components (simple pages) + +Create two exports — one for each system: + +```tsx +// src/components/MyPage/MyPage.tsx +import { Content, PageWithHeader } from '@backstage/core-components'; + +// Legacy — includes page shell +export function MyPage() { + return ( + + + + + + ); +} + +// NFS — content only, no page shell +export function NfsMyPage() { + return ( + + + + ); +} +``` + +The NFS variant is loaded by `PageBlueprint`: + +```tsx +loader: () => import('./components/MyPage').then(m => ), +``` + +If the NFS page needs a subtitle or custom actions below the framework header, use `Header` from `@backstage/ui`: + +```tsx +import { Header } from '@backstage/ui'; + +export function NfsMyPage() { + return ( + <> +
Help} + /> + + + + + ); +} +``` + +### Pattern B: Header variant prop (complex pages) + +For pages with significant shared logic, use a prop to switch between systems: + +```tsx +function MyPageContent(props: MyPageProps & { headerVariant: 'legacy' | 'bui' }) { + const { headerVariant, ...rest } = props; + const pageContent = {/* shared page body */}; + + if (headerVariant === 'bui') { + return pageContent; + } + return ( + + {pageContent} + + ); +} + +// Old system export +export const MyPage = (props: MyPageProps) => ( + +); + +// NFS export +export const NfsMyPage = (props: MyPageProps) => ( + +); +``` + +## SubPageBlueprint — tabbed sub-pages + +For plugins with tabbed pages, use `SubPageBlueprint` instead of internal routing. The parent `PageBlueprint` omits its `loader` — the framework renders sub-pages as tabs automatically. + +```tsx +import { PageBlueprint, SubPageBlueprint } from '@backstage/frontend-plugin-api'; + +// Parent page — no loader, renders tabs +const myPluginPage = PageBlueprint.make({ + params: { + path: '/my-plugin', + routeRef: rootRouteRef, + }, +}); + +// Sub-pages — path is relative (no leading /) +const overviewPage = SubPageBlueprint.make({ + name: 'overview', + params: { + path: 'overview', + title: 'Overview', + loader: () => import('./components/Overview').then(m => ), + }, +}); + +const settingsPage = SubPageBlueprint.make({ + name: 'settings', + params: { + path: 'settings', + title: 'Settings', + loader: () => import('./components/Settings').then(m => ), + }, +}); +``` + +**When to use `SubPageBlueprint`:** Only for top-level tabs that should appear in the page header. For drill-down routes (e.g. `/items/:id`), keep internal routing inside a `PageBlueprint` loader. + +## ApiBlueprint — replaces `apis` array in `createPlugin` + +```tsx +import { ApiBlueprint, discoveryApiRef, fetchApiRef } from '@backstage/frontend-plugin-api'; +import { myApiRef, MyApiClient } from './api'; + +const myApi = ApiBlueprint.make({ + params: defineParams => defineParams({ + api: myApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new MyApiClient({ discoveryApi, fetchApi }), + }), +}); +``` + +**API refs in factories** must come from `@backstage/frontend-plugin-api` (not `core-plugin-api`): `configApiRef`, `fetchApiRef`, `identityApiRef`, `discoveryApiRef`, etc. + +**API refs in components** should stay on `@backstage/core-plugin-api` so the same components work in both systems. + +### API ownership + +Each API has an owner plugin. Only modules targeting the owning `pluginId` can override it. Ownership is determined by: + +1. Explicit `pluginId` on the API ref (recommended): + ```tsx + const myApiRef = createApiRef().with({ + id: 'plugin.my-plugin.client', + pluginId: 'my-plugin', + }); + ``` +2. ID pattern: `plugin..*` → owned by that plugin +3. `core.*` → owned by the `app` plugin + +If you try to override an API from a module with the wrong `pluginId`, you get `API_FACTORY_CONFLICT`. + +## Route refs + +### Reuse existing route refs + +Route refs from `@backstage/core-plugin-api` work directly in NFS — no conversion needed. Pass them to `createFrontendPlugin`'s `routes` and to `PageBlueprint`'s `routeRef`. + +### External route refs with `defaultTarget` + +Set `defaultTarget` on external route refs so plugins work out-of-the-box without requiring `bindRoutes` in the app: + +```tsx +export const viewTechDocRouteRef = createExternalRouteRef({ + id: 'view-techdoc', + optional: true, + params: ['namespace', 'kind', 'name'], + defaultTarget: 'techdocs.docRoot', +}); +``` + +The target format is `.`, matching the `routes` map of the target plugin. The default is only used when the target plugin is installed. + +### `useRouteRef` behavior difference + +In 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 components, handle the `undefined` case. + +## Assembling in the plugin + +```tsx +import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; +import { RiToolsLine } from '@remixicon/react'; + +export default createFrontendPlugin({ + pluginId: 'my-plugin', + title: 'My Plugin', + icon: , + extensions: [myPage, myApi], + routes: { + root: rootRouteRef, + }, + externalRoutes: { + // same external routes as the old plugin + }, +}); +``` + +Pages and APIs go into the `extensions` array. Nav items are auto-generated from pages with `title` + `icon` + `routeRef`. For icons, prefer [Remix Icons](https://remixicon.com/) from `@remixicon/react`. diff --git a/skills/nfs-migration/references/migrate-rhdh-extensions.md b/skills/nfs-migration/references/migrate-rhdh-extensions.md new file mode 100644 index 0000000..54a99cb --- /dev/null +++ b/skills/nfs-migration/references/migrate-rhdh-extensions.md @@ -0,0 +1,112 @@ +# RHDH-Specific Extension Migration + +These blueprints are unique to Red Hat Developer Hub or have RHDH-specific usage patterns. For the full mount point → blueprint mapping, see `mount-point-mapping.md`. + +## AppDrawerContentBlueprint — drawer panels + +Replaces `application/internal/drawer-content` mount point. + +```tsx +import { AppDrawerContentBlueprint } from '@red-hat-developer-hub/backstage-plugin-app-react/alpha'; + +const myDrawer = AppDrawerContentBlueprint.make({ + name: 'my-drawer', + params: { + id: MY_DRAWER_ID, + element: , + resizable: true, + defaultWidth: 400, + }, +}); +``` + +Register in your plugin's `extensions` array. + +Drawer content mounts/unmounts with the drawer. Init logic that should persist (auto-open triggers, event listeners, snackbar setup) goes in a separate `AppRootElementBlueprint` registered via `createFrontendModule({ pluginId: 'app' })`: + +```tsx +const myInitElement = AppRootElementBlueprint.make({ + name: 'my-init', + params: { element: }, +}); + +export const myInitModule = createFrontendModule({ + pluginId: 'app', + extensions: [myInitElement], +}); +``` + +## GlobalHeaderMenuItemBlueprint — header menu items + +Replaces `global.header/*` and `header/*` mount points. + +```tsx +import { GlobalHeaderMenuItemBlueprint } from '@red-hat-developer-hub/backstage-plugin-global-header/alpha'; + +const myMenuItem = GlobalHeaderMenuItemBlueprint.make({ + name: 'my-menu-item', + params: { + target: 'help', + component: MyMenuItem, + priority: 50, + }, +}); +``` + +- `target`: the header section (`help`, `profile`, `create`, etc.) +- `component`: React component for the menu item +- `priority`: ordering within the section (higher = first) + +If your plugin owns the menu item, include it in the plugin's `extensions` array. If injecting from outside, use `createFrontendModule({ pluginId: 'global-header' })`. + +## HomePageWidgetBlueprint — homepage cards + +Replaces `home.page/cards` and `home.page/widgets` mount points. + +```tsx +import { HomePageWidgetBlueprint } from '@backstage/plugin-home-react/alpha'; + +const myWidget = HomePageWidgetBlueprint.make({ + name: 'my-widget', + params: { + name: 'My Widget', + layout: { + width: { minColumns: 4, maxColumns: 12, defaultColumns: 12 }, + height: { minRows: 2, maxRows: 12, defaultRows: 4 }, + }, + components: () => import('./components/MyWidget').then(m => ({ + Content: m.MyWidget, + })), + }, +}); +``` + +Note: `HomePageWidgetBlueprint` uses `components` (returning `{ Content }`) and `layout` — different from other blueprints that use `loader`. + +Register via module targeting the home plugin: + +```tsx +export const myHomeModule = createFrontendModule({ + pluginId: 'home', + extensions: [myWidget], +}); +``` + +## AppRootWrapperBlueprint — app-level providers + +Replaces `application/provider` mount point. Import from `@backstage/plugin-app-react`. + +```tsx +import { AppRootWrapperBlueprint } from '@backstage/plugin-app-react'; + +const myWrapper = AppRootWrapperBlueprint.make({ + name: 'my-provider', + params: { + component: ({ children }) => ( + {children} + ), + }, +}); +``` + +Register via `createFrontendModule({ pluginId: 'app' })`. diff --git a/skills/nfs-migration/references/migrate-translations.md b/skills/nfs-migration/references/migrate-translations.md new file mode 100644 index 0000000..8f10f55 --- /dev/null +++ b/skills/nfs-migration/references/migrate-translations.md @@ -0,0 +1,55 @@ +# Translation Migration + +Translations target the **app plugin** (`pluginId: 'app'`), so they must use `createFrontendModule`, **not** be included in your plugin's extensions array. + +## TranslationBlueprint + +```tsx +import { TranslationBlueprint } from '@backstage/plugin-app-react'; +import { myTranslationResource } from './translations'; + +const translationExtension = TranslationBlueprint.make({ + params: { + resource: myTranslationResource, + }, +}); +``` + +## Register as an app module + +```tsx +import { createFrontendModule } from '@backstage/frontend-plugin-api'; + +export const myTranslationsModule = createFrontendModule({ + pluginId: 'app', + extensions: [translationExtension], +}); +``` + +## Export separately + +In your plugin's `src/index.ts`: + +```tsx +export { default as default } from './plugin'; +export { myTranslationsModule } from './modules'; +``` + +## App integration + +The consuming app must include the module in its `features` array: + +```tsx +import myPlugin, { myTranslationsModule } from '@scope/my-plugin'; + +createApp({ + features: [myPlugin, myTranslationsModule], +}); +``` + +## Key rules + +- **Always** `pluginId: 'app'` — translations are app-level, not plugin-level +- Each language gets its own `createTranslationResource` call +- Export the module as a named export alongside the default plugin export +- The app must explicitly opt in by adding the module to `features` diff --git a/skills/nfs-migration/references/mount-point-mapping.md b/skills/nfs-migration/references/mount-point-mapping.md new file mode 100644 index 0000000..8d0e4ca --- /dev/null +++ b/skills/nfs-migration/references/mount-point-mapping.md @@ -0,0 +1,215 @@ +# RHDH Mount Point → NFS Blueprint Mapping + +RHDH's legacy dynamic plugin system used mount points in `app-config.dynamic.yaml` to place components. In NFS, these are replaced by extension blueprints. This reference maps each mount point to its NFS equivalent. + +## Quick reference + +| Legacy mount point | NFS Blueprint | Package | +|---|---|---| +| `dynamicRoutes` (path + importName) | `PageBlueprint` | `@backstage/frontend-plugin-api` | +| `menuItem` in dynamicRoutes | Auto-discovered from `PageBlueprint` title/icon | — | +| `entity.page.*/cards` | `EntityContentBlueprint` | `@backstage/plugin-catalog-react/alpha` | +| `home.page/cards`, `home.page/widgets` | `HomePageWidgetBlueprint` | `@backstage/plugin-home-react/alpha` | +| `application/listener` | `AppRootElementBlueprint` | `@backstage/frontend-plugin-api` | +| `application/provider` | `AppRootWrapperBlueprint` | `@backstage/plugin-app-react` | +| `application/internal/drawer-content` | `AppDrawerContentBlueprint` | `@red-hat-developer-hub/backstage-plugin-app-react/alpha` | +| `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` | — | + +## Dynamic routes → PageBlueprint + +**Before (mount point):** +```yaml +dynamicRoutes: + - path: /my-plugin + importName: MyPluginPage + menuItem: + icon: myIcon + text: My Plugin +``` + +**After (NFS):** +```tsx +const myPage = PageBlueprint.make({ + params: { + path: '/my-plugin', + title: 'My Plugin', + icon: , + routeRef: rootRouteRef, + loader: () => import('./components/MyPage').then(m => ), + }, +}); +``` + +The `menuItem` config is no longer needed — nav items are auto-discovered from pages with `title` + `icon` + `routeRef`. + +## Entity page mount points → EntityContentBlueprint + +**Before (mount point):** +```yaml +mountPoints: + - mountPoint: entity.page.workflows/cards + importName: OrchestratorCatalogTab + config: + if: + allOf: + - isKind: component +``` + +**After (NFS):** +```tsx +const entityContent = EntityContentBlueprint.make({ + name: 'workflows', + params: { + path: '/workflows', + title: 'Workflows', + filter: 'kind:component', + loader: () => import('./components/OrchestratorCatalogTab').then(m => ), + }, +}); +``` + +Register in the plugin's `extensions` array. + +## Homepage widgets → HomePageWidgetBlueprint + +**Before (mount point):** +```yaml +mountPoints: + - mountPoint: home.page/cards + importName: OnboardingSection + config: + layouts: + xl: { w: 12, h: 6 } +``` + +**After (NFS):** +```tsx +import { HomePageWidgetBlueprint } from '@backstage/plugin-home-react/alpha'; + +const myWidget = HomePageWidgetBlueprint.make({ + name: 'my-widget', + params: { + name: 'My Widget', + layout: { + width: { minColumns: 4, maxColumns: 12, defaultColumns: 12 }, + height: { minRows: 2, maxRows: 12, defaultRows: 4 }, + }, + components: () => import('./components/MyWidget').then(m => ({ + Content: m.MyWidget, + })), + }, +}); +``` + +Note: `HomePageWidgetBlueprint` uses `components` (returning `{ Content }`) instead of `loader`. + +## Drawer content → AppDrawerContentBlueprint + +**Before (mount point):** +```yaml +mountPoints: + - mountPoint: application/internal/drawer-content + importName: MyDrawerContent +``` + +**After (NFS):** +```tsx +import { AppDrawerContentBlueprint } from '@red-hat-developer-hub/backstage-plugin-app-react/alpha'; + +const myDrawer = AppDrawerContentBlueprint.make({ + name: 'my-drawer', + params: { + id: MY_DRAWER_ID, + element: , + resizable: true, + defaultWidth: 400, + }, +}); +``` + +Register in the plugin's `extensions` array. + +Init logic that should persist across drawer toggles (e.g. auto-open triggers, event listeners) goes in a separate `AppRootElementBlueprint` registered via `createFrontendModule({ pluginId: 'app' })`. + +## App-level listeners → AppRootElementBlueprint + +**Before (mount point):** +```yaml +mountPoints: + - mountPoint: application/listener + importName: MyFAB +``` + +**After (NFS):** +```tsx +import { AppRootElementBlueprint } from '@backstage/frontend-plugin-api'; + +const myElement = AppRootElementBlueprint.make({ + name: 'my-fab', + params: { + element: , + }, +}); +``` + +## App-level providers → AppRootWrapperBlueprint + +**Before (mount point):** +```yaml +mountPoints: + - mountPoint: application/provider + importName: MyProvider +``` + +**After (NFS):** +```tsx +import { AppRootWrapperBlueprint } from '@backstage/plugin-app-react'; + +const myWrapper = AppRootWrapperBlueprint.make({ + name: 'my-provider', + params: { + component: ({ children }) => ( + {children} + ), + }, +}); +``` + +Register via `createFrontendModule({ pluginId: 'app' })`. + +## Global header items → GlobalHeaderMenuItemBlueprint + +**Before (mount point):** +```yaml +mountPoints: + - mountPoint: global.header/help + importName: MyHelpMenuItem +``` + +**After (NFS):** +```tsx +import { GlobalHeaderMenuItemBlueprint } from '@red-hat-developer-hub/backstage-plugin-global-header/alpha'; + +const myMenuItem = GlobalHeaderMenuItemBlueprint.make({ + name: 'my-help-item', + params: { + target: 'help', + component: MyHelpMenuItem, + priority: 50, + }, +}); +``` + +The `target` param maps to the header section: `help`, `profile`, `create`, etc. Use `priority` to control ordering. + +## Real migration examples + +| Plugin | Mount points used | NFS blueprints | PR | +|--------|------------------|----------------|-----| +| lightspeed | `application/listener`, `application/internal/drawer-content` | `AppRootWrapperBlueprint` (FAB), `AppDrawerContentBlueprint`, `PageBlueprint` | [#2721](https://github.com/redhat-developer/rhdh-plugins/pull/2721) | +| quickstart | `application/provider`, `application/internal/drawer-state`, `global.header/help` | `AppDrawerContentBlueprint`, `GlobalHeaderMenuItemBlueprint`, `AppRootElementBlueprint` | [#2842](https://github.com/redhat-developer/rhdh-plugins/pull/2842) | +| homepage | `home.page/cards` | `HomePageWidgetBlueprint` | [#2423](https://github.com/redhat-developer/rhdh-plugins/pull/2423) | +| orchestrator | `entity.page.workflows/cards` | `EntityContentBlueprint` | [#2526](https://github.com/redhat-developer/rhdh-plugins/pull/2526) | diff --git a/skills/nfs-migration/references/package-json.md b/skills/nfs-migration/references/package-json.md new file mode 100644 index 0000000..3570c48 --- /dev/null +++ b/skills/nfs-migration/references/package-json.md @@ -0,0 +1,87 @@ +# Package.json Export Configuration + +## Direct to GA (recommended) + +NFS is the root export. Legacy moves to `./legacy`. + +```json +{ + "exports": { + ".": "./src/index.ts", + "./legacy": "./src/legacy.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "legacy": ["src/legacy.ts"], + "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 + +## Phased approach + +NFS at `./alpha`, legacy stays at root. + +```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 + +## Required backstage fields + +Ensure these exist in `package.json`: + +```json +{ + "backstage": { + "role": "frontend-plugin", + "pluginId": "my-plugin", + "pluginPackages": [ + "@scope/backstage-plugin-my-plugin" + ] + } +} +``` + +- `role`: must be `frontend-plugin` +- `pluginId`: must match the `pluginId` passed to `createFrontendPlugin` +- `pluginPackages`: array of all packages in this plugin family (frontend, backend, common, etc.) + +## Checklist + +- [ ] `exports` field has `.` pointing to NFS entry +- [ ] `typesVersions` mirrors any sub-path exports +- [ ] `publishConfig` has types/default for each sub-path (GA approach) +- [ ] `backstage.role` is `frontend-plugin` +- [ ] `backstage.pluginId` matches `createFrontendPlugin({ pluginId: '...' })` diff --git a/skills/nfs-migration/references/reference-prs.md b/skills/nfs-migration/references/reference-prs.md new file mode 100644 index 0000000..4d78e33 --- /dev/null +++ b/skills/nfs-migration/references/reference-prs.md @@ -0,0 +1,24 @@ +# Reference Migration PRs + +Real-world NFS migration PRs from the `rhdh-plugins` repository. Use these as patterns when migrating similar plugins. + +| Plugin | PR | What to learn | Complexity | +|--------|-----|---------------|------------| +| adoption-insights | [#2309](https://github.com/redhat-developer/rhdh-plugins/pull/2309) | Simple page plugin: Page + Nav + API blueprints | Low — good starting point | +| bulk-import | [#2247](https://github.com/redhat-developer/rhdh-plugins/pull/2247) | Page + Nav + permission-based access patterns | Low-Medium — adds permission handling | +| scorecard | [#2487](https://github.com/redhat-developer/rhdh-plugins/pull/2487) | EntityContent + HomePageWidget blueprints | Medium — multi-extension-type migration | +| orchestrator | [#2526](https://github.com/redhat-developer/rhdh-plugins/pull/2526) | EntityContent + multiple routes/pages | Medium — complex routing with entity integration | +| lightspeed | [#2721](https://github.com/redhat-developer/rhdh-plugins/pull/2721) | Drawer + FAB using RHDH-specific blueprints | Medium — RHDH-specific extensions (AppDrawerContent + AppRootElement) | +| extensions | [#2527](https://github.com/redhat-developer/rhdh-plugins/pull/2527) | `compatWrapper` usage for legacy components | Medium — bridging legacy and NFS | +| homepage | [#2423](https://github.com/redhat-developer/rhdh-plugins/pull/2423) | HomePageWidgets + compatWrapper | Medium — homepage integration with legacy compat | +| quickstart | [#2842](https://github.com/redhat-developer/rhdh-plugins/pull/2842) | Drawer + GlobalHeaderMenuItem | Medium-High — RHDH drawer + header menu integration | + +## How to use these + +1. Find the PR closest to your plugin's extension types +2. Read the PR's file changes to see the migration pattern +3. Pay attention to: + - How extensions are split between plugin and modules + - How `package.json` exports are configured + - How the dev app is set up + - How legacy code is handled (kept vs removed) diff --git a/skills/nfs-migration/references/support.md b/skills/nfs-migration/references/support.md new file mode 100644 index 0000000..cd8dc48 --- /dev/null +++ b/skills/nfs-migration/references/support.md @@ -0,0 +1,16 @@ +# Getting Help + +## Resources + +- **[RHDH Plugins GitHub Issues](https://github.com/redhat-developer/rhdh-plugins/issues)** — Plugin-specific questions, bug reports, and feature requests for RHDH plugins +- **[Backstage Discord](https://discord.gg/backstage-687207715902193673)** — Community support, real-time help from maintainers and other developers +- **[Backstage GitHub Discussions](https://github.com/backstage/backstage/discussions)** — Upstream questions about Backstage core, NFS architecture, and API design +- **[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 + +## When to escalate + +- **Build failures after migration** — Check `references/gotchas.md` first, then file an issue +- **Blueprint not behaving as expected** — Check upstream Backstage docs for the latest API, then ask on Discord +- **RHDH-specific blueprint issues** — File an issue on `rhdh-plugins` repo with the `nfs` label +- **Dynamic plugin loading failures** — Check module federation config, then consult RHDH docs diff --git a/skills/nfs-migration/references/testing-rhdh.md b/skills/nfs-migration/references/testing-rhdh.md new file mode 100644 index 0000000..6e751b1 --- /dev/null +++ b/skills/nfs-migration/references/testing-rhdh.md @@ -0,0 +1,62 @@ +# Testing with RHDH + +## Local testing (via rhdh-local skill) + +1. **Export as dynamic plugin** — Ensure the plugin is packaged for dynamic loading (check `package.json` for dynamic plugin config) + +2. **Enable NFS app** — If NFS is not yet the default, set these environment variables: + ``` + APP_CONFIG_app_packageName=app-next + ENABLE_STANDARD_MODULE_FEDERATION=true + ``` + +3. **Deploy locally** — Use the `rhdh-local` skill to deploy and test: + ``` + Read ../rhdh-local/SKILL.md and follow its instructions + ``` + +4. **Verify** — Confirm the plugin appears in the NFS app: + - Page loads at expected route + - Nav item visible in sidebar + - API calls succeed + - Entity tabs appear (if applicable) + +## Cluster testing (OpenShift/K8s) + +1. **Package the plugin** — Build as OCI image or tgz archive: + ```bash + # OCI (preferred for OpenShift) + yarn export-dynamic --tag my-registry/my-plugin:latest + + # Or tgz + yarn pack + ``` + +2. **Add to dynamic-plugins.yaml** in your RHDH deployment: + ```yaml + plugins: + - package: 'oci://my-registry/my-plugin:latest' + disabled: false + pluginConfig: {} + ``` + +3. **Set NFS env vars** (if NFS is not default): + ```yaml + env: + - name: APP_CONFIG_app_packageName + value: app-next + - name: ENABLE_STANDARD_MODULE_FEDERATION + value: 'true' + ``` + +4. **Verify via RHDH UI** — Access the RHDH instance and confirm: + - Plugin loads without console errors + - All extensions render correctly + - Dynamic plugin config is picked up + +## Troubleshooting + +- **Plugin not loading**: Check `dynamic-plugins.yaml` syntax and that the package reference is correct +- **Module federation errors**: Ensure `app-react` is shared as singleton in the webpack config +- **Missing context**: Wrap components with `compatWrapper()` if they need legacy providers +- **Blank page**: Check browser console for import errors — likely a missing export or wrong path diff --git a/skills/nfs-migration/references/verification.md b/skills/nfs-migration/references/verification.md new file mode 100644 index 0000000..2a43b60 --- /dev/null +++ b/skills/nfs-migration/references/verification.md @@ -0,0 +1,84 @@ +# Migration Verification + +## Smoke-test checklist + +Run these in order. Stop and fix any failures before continuing. + +1. **`yarn tsc`** — TypeScript compilation passes with no errors +2. **`yarn build`** — Package builds successfully +3. **Default export check** — `src/index.ts` default-exports a `createFrontendPlugin` result +4. **Extensions array** — All blueprints are listed in the `extensions` array +5. **Start the app** — `yarn start` (or dev app equivalent), page loads at expected path +6. **Nav item** — Sidebar shows the plugin's nav entry +7. **API calls** — Open browser DevTools Network tab, verify API requests succeed +8. **Entity tabs** (if applicable) — Navigate to an entity, verify tab appears +9. **Translations** (if applicable) — Switch language, verify strings update + +## Playwright smoke test (optional) + +```ts +import { test, expect } from '@playwright/test'; + +test('plugin page renders', async ({ page }) => { + await page.goto('/my-plugin'); + await expect(page.locator('h1')).toContainText('My Plugin'); +}); + +test('nav item visible', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('nav')).toContainText('My Plugin'); +}); + +test('entity tab visible', async ({ page }) => { + await page.goto('/catalog/default/component/my-component'); + await expect(page.locator('[role="tab"]')).toContainText('My Plugin'); +}); +``` + +Adapt selectors to your plugin. These are starting points, not production-ready tests. + +## Testing principles (any framework) + +| What to verify | How | +|---------------|-----| +| Extension registration | All blueprints present in `extensions` array | +| Page shell removed | NFS page components don't include `PageWithHeader` — framework provides the header | +| Route resolution | Page accessible at its declared `path` | +| API availability | API blueprint provides the correct client instance | +| Nav items | Page with `title` + `icon` + `routeRef` appears in sidebar automatically | +| Entity tabs | Visible on entity pages for matching entity filter | +| Entity cards | Visible on entity overview for matching filter | +| Translations | Language switching renders translated strings | +| App wrappers | Provider context available to child components | +| 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 | + +## 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: + +```bash +# Find any imports of legacy named exports from the plugin's root +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'`. + +## Quick validation commands + +```bash +# Type check (from workspace root — catches consumer import issues too) +yarn tsc + +# Build +yarn build + +# Lint (if configured) +yarn lint + +# Start NFS dev app +yarn start + +# Start legacy dev app (if kept) +yarn start:legacy +``` diff --git a/skills/nfs-migration/workflows/test-nfs-plugin.md b/skills/nfs-migration/workflows/test-nfs-plugin.md new file mode 100644 index 0000000..6cb1857 --- /dev/null +++ b/skills/nfs-migration/workflows/test-nfs-plugin.md @@ -0,0 +1,81 @@ +# Test NFS Plugin in RHDH + + +- Migrated plugin with NFS exports (run the migration workflow first) +- Container runtime (podman or docker) for local testing +- Access to an RHDH instance (local or cluster) for end-to-end verification + + + + +## Phase 1: Export as Dynamic Plugin + +1. Build the plugin: `yarn build` +2. Export for dynamic loading. If using the `create-plugin` skill's export command: + ```bash + python scripts/export-plugin.py --plugin-dir plugins/my-plugin --format tgz + ``` + Otherwise, use `@janus-idp/cli` or `backstage-cli` to export. + +## Phase 2: Local Testing + +### Option A: NFS is the default app (GA and later) + +1. Start local RHDH using the `rhdh-local` skill: + Read `../rhdh-local/SKILL.md` and follow the "Enable a plugin" workflow. +2. Verify the plugin loads in the UI. + +### Option B: NFS not yet default (pre-GA) + +1. Set environment variables: + ```bash + APP_CONFIG_app_packageName=app-next + ENABLE_STANDARD_MODULE_FEDERATION=true + ``` +2. Start local RHDH with these vars. +3. Verify the plugin loads in the NFS app shell. + +### Verification Steps (Local) + +- [ ] Plugin page is accessible at its declared path +- [ ] Nav item appears in the sidebar +- [ ] API calls succeed (check browser network tab) +- [ ] Entity tabs appear on matching entity pages (if applicable) +- [ ] Translations load correctly (if applicable) +- [ ] No console errors related to the plugin + +## Phase 3: Cluster Testing (OpenShift / Kubernetes) + +1. Package the plugin as OCI image or tgz archive. +2. Push to your container registry (e.g. quay.io). +3. Add to your RHDH deployment's `dynamic-plugins.yaml`: + ```yaml + plugins: + - package: 'oci://quay.io/your-org/your-plugin:latest!your-plugin' + disabled: false + ``` +4. If NFS is not the default, add to your RHDH Helm values or operator config: + ```yaml + extraEnvVars: + - name: APP_CONFIG_app_packageName + value: app-next + - name: ENABLE_STANDARD_MODULE_FEDERATION + value: "true" + ``` +5. Restart the RHDH pod and verify. + +### Verification Steps (Cluster) + +- [ ] Pod starts without errors +- [ ] Plugin appears in the RHDH UI +- [ ] All extension types render correctly +- [ ] No errors in pod logs related to the plugin + + + + +- Plugin loads successfully in the RHDH NFS app shell +- All extensions (pages, nav items, entity tabs, etc.) render correctly +- No console or pod log errors related to the plugin +- API calls from the plugin succeed + diff --git a/skills/rhdh/SKILL.md b/skills/rhdh/SKILL.md index 381a956..72fd0e3 100644 --- a/skills/rhdh/SKILL.md +++ b/skills/rhdh/SKILL.md @@ -194,6 +194,14 @@ What would you like to do? **To route:** Read `../rhdh-release/SKILL.md` and follow its intake process. +### Backstage Upgrade Routes + +| Response | Skill | +|----------|-------| +| "upgrade backstage", "bump backstage", "update @backstage", "backstage version", "align deps", "versions:bump" | Route to `backstage-upgrade` skill | + +**To route:** Read `../backstage-upgrade/SKILL.md` and follow its intake process. + ### General Routes | Response | Action |