diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c4e8a..25ff44a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to `@red-hat-developer-hub/cli` are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.0.9 - 2026-09-15 + +### Added + +- **`plugin new`:** Add isolated local development harnesses and `yarn start` scripts for generated frontend and backend plugins. + ## 2.0.8 - 2026-09-15 ### Fixed diff --git a/README.md b/README.md index 937bde2..2339407 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Use `plugin new` to create a standalone, version-pinned dynamic plugin project: rhdh-cli plugin new my-plugin --type frontend --rhdh-version 2.1.0 ``` -Supported types are `frontend` (a New Frontend System, or NFS, page), `backend` (a minimal new-backend-system plugin), and `catalog-processor-module` (a catalog processor module). Use `--name ` as an alternative to the positional name, and `--output ` to select a destination. The generated project uses the target RHDH release's Backstage manifest for every `@backstage/*` dependency. For air-gapped environments, provide `--manifest-file` and set `RHDH_OFFLINE=true`. Export and package generated plugins with `npx @red-hat-developer-hub/cli`, or through RHDH Dynamic Plugin Factory, rather than adding the CLI as a project dependency. +Supported types are `frontend` (a New Frontend System, or NFS, page), `backend` (a minimal new-backend-system plugin), and `catalog-processor-module` (a catalog processor module). Frontend and backend projects include a `dev/` harness and `yarn start` for isolated development; catalog processor modules do not because they require a host backend plugin. Use `--name ` as an alternative to the positional name, and `--output ` to select a destination. The generated project uses the target RHDH release's Backstage manifest for every `@backstage/*` dependency. For air-gapped environments, provide `--manifest-file` and set `RHDH_OFFLINE=true`. Export and package generated plugins with `npx @red-hat-developer-hub/cli`, or through RHDH Dynamic Plugin Factory, rather than adding the CLI as a project dependency. ## Development diff --git a/package.json b/package.json index 45afae5..f2f7a0a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@red-hat-developer-hub/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "2.0.8", + "version": "2.0.9", "publishConfig": { "access": "public" }, diff --git a/src/commands/new/__snapshots__/command.test.ts.snap b/src/commands/new/__snapshots__/command.test.ts.snap index 3d016b4..10ba274 100644 --- a/src/commands/new/__snapshots__/command.test.ts.snap +++ b/src/commands/new/__snapshots__/command.test.ts.snap @@ -59,12 +59,14 @@ RHDH Dynamic Plugin Factory is the preferred workflow for repeatable build and p "types": "src/index.ts", "backstage": { "role": "backend-plugin", "pluginId": "example-plugin" }, "scripts": { + "start": "backstage-cli package start", "build": "yarn tsc && backstage-cli package build", "tsc": "tsc --declaration", "test": "backstage-cli package test --passWithNoTests" }, "dependencies": { "@backstage/backend-plugin-api": "1.10.0" }, "devDependencies": { + "@backstage/backend-defaults": "0.12.5", "@backstage/cli": "0.36.5", "@types/jest": "^29.5.14", "jest": "^29.7.0", @@ -114,12 +116,22 @@ export default plugin; "tsconfig.json", "{ "extends": "@backstage/cli/config/tsconfig.json", - "include": ["src"], + "include": ["src", "dev"], "compilerOptions": { "outDir": "dist-types", "rootDir": "." } } +", + ], + [ + "dev/index.ts", + "import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('../src')); +backend.start(); ", ], ] @@ -305,10 +317,26 @@ RHDH Dynamic Plugin Factory is the preferred workflow for repeatable build and p "packageManager": "yarn@4.17.1", "private": true, "license": "Apache-2.0", + "files": ["dist"], "main": "src/index.ts", "types": "src/index.ts", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "package.json": ["package.json"] + } + }, "backstage": { "role": "frontend-plugin", "pluginId": "example-plugin" }, "scripts": { + "start": "backstage-cli package start", "build": "yarn tsc && backstage-cli package build", "tsc": "tsc --declaration", "test": "backstage-cli package test --passWithNoTests" @@ -319,6 +347,7 @@ RHDH Dynamic Plugin Factory is the preferred workflow for repeatable build and p }, "devDependencies": { "@backstage/cli": "0.36.5", + "@backstage/frontend-dev-utils": "0.4.5", "@types/jest": "^29.5.14", "@types/react": "^18.0.0", "react": "^18.0.0", @@ -338,9 +367,26 @@ RHDH Dynamic Plugin Factory is the preferred workflow for repeatable build and p ], [ "src/index.ts", + "export { plugin as default } from './plugin'; +", + ], + [ + "tsconfig.json", + "{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src", "dev"], + "compilerOptions": { + "jsx": "react-jsx", + "outDir": "dist-types", + "rootDir": "." + } +} +", + ], + [ + "src/plugin.tsx", "import { createRouteRef } from '@backstage/core-plugin-api'; import { createFrontendPlugin, PageBlueprint } from '@backstage/frontend-plugin-api'; -import React from 'react'; const rootRouteRef = createRouteRef({ id: 'example-plugin' }); const page = PageBlueprint.make({ @@ -348,11 +394,11 @@ const page = PageBlueprint.make({ title: 'example-plugin', path: '/example-plugin', routeRef: rootRouteRef, - loader: () => import('./PluginPage').then(m => React.createElement(m.PluginPage)), + loader: () => import('./PluginPage').then(m => ), }, }); -export default createFrontendPlugin({ +export const plugin = createFrontendPlugin({ pluginId: 'example-plugin', extensions: [page], routes: { root: rootRouteRef }, @@ -360,24 +406,19 @@ export default createFrontendPlugin({ ", ], [ - "tsconfig.json", - "{ - "extends": "@backstage/cli/config/tsconfig.json", - "include": ["src"], - "compilerOptions": { - "outDir": "dist-types", - "rootDir": "." - } + "src/PluginPage.tsx", + "export function PluginPage() { + return

example-plugin

; } ", ], [ - "src/PluginPage.tsx", - "import React from 'react'; + "dev/index.tsx", + "import { createDevApp } from '@backstage/frontend-dev-utils'; -export function PluginPage() { - return React.createElement('h1', undefined, 'example-plugin'); -} +import plugin from '../src'; + +createDevApp({ features: [plugin] }); ", ], ] diff --git a/src/commands/new/command.test.ts b/src/commands/new/command.test.ts index 284ead0..2280af1 100644 --- a/src/commands/new/command.test.ts +++ b/src/commands/new/command.test.ts @@ -39,10 +39,12 @@ describe('createPluginProject', () => { backstageVersion: '1.54.0', source: 'matrix', packages: new Map([ + ['@backstage/backend-defaults', '0.12.5'], ['@backstage/backend-plugin-api', '1.10.0'], ['@backstage/catalog-model', '1.10.0'], ['@backstage/cli', '0.36.5'], ['@backstage/core-plugin-api', '1.12.7'], + ['@backstage/frontend-dev-utils', '0.4.5'], ['@backstage/frontend-plugin-api', '0.17.2'], ['@backstage/plugin-catalog-node', '2.2.4'], ]), @@ -55,7 +57,7 @@ describe('createPluginProject', () => { }); it.each([ - ['frontend', 'src/index.ts', 'PageBlueprint'], + ['frontend', 'src/index.ts', 'export { plugin as default }'], ['backend', 'src/index.ts', 'createBackendPlugin'], [ 'catalog-processor-module', @@ -95,6 +97,14 @@ describe('createPluginProject', () => { '@red-hat-developer-hub/cli', ); expect(packageJson.packageManager).toBe('yarn@4.17.1'); + expect(packageJson.files).toEqual( + type === 'frontend' ? ['dist'] : undefined, + ); + expect(packageJson.scripts.start).toBe( + type === 'catalog-processor-module' + ? undefined + : 'backstage-cli package start', + ); await expect( fs.readFile(path.join(output, '.yarnrc.yml'), 'utf8'), ).resolves.toBe('nodeLinker: node-modules\n'); @@ -113,9 +123,12 @@ describe('createPluginProject', () => { 'README.md', 'backstage.json', 'package.json', - 'src/index.ts', + entryPoint, 'tsconfig.json', - ...(type === 'frontend' ? ['src/PluginPage.tsx'] : []), + ...(type === 'frontend' + ? ['src/plugin.tsx', 'src/PluginPage.tsx', 'dev/index.tsx'] + : []), + ...(type === 'backend' ? ['dev/index.ts'] : []), ].map(async file => [ file, await fs.readFile(path.join(output, file), 'utf8'), diff --git a/templates/plugin-new/backend/dev/index.ts b/templates/plugin-new/backend/dev/index.ts new file mode 100644 index 0000000..8670438 --- /dev/null +++ b/templates/plugin-new/backend/dev/index.ts @@ -0,0 +1,6 @@ +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('../src')); +backend.start(); diff --git a/templates/plugin-new/backend/package.json.hbs b/templates/plugin-new/backend/package.json.hbs index 8ef1e7f..22efb5a 100644 --- a/templates/plugin-new/backend/package.json.hbs +++ b/templates/plugin-new/backend/package.json.hbs @@ -8,12 +8,14 @@ "types": "src/index.ts", "backstage": { "role": "backend-plugin", "pluginId": "{{pluginId}}" }, "scripts": { + "start": "backstage-cli package start", "build": "yarn tsc && backstage-cli package build", "tsc": "tsc --declaration", "test": "backstage-cli package test --passWithNoTests" }, "dependencies": { "@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}" }, "devDependencies": { + "@backstage/backend-defaults": "{{versionQuery '@backstage/backend-defaults'}}", "@backstage/cli": "{{versionQuery '@backstage/cli'}}", "@types/jest": "^29.5.14", "jest": "^29.7.0", diff --git a/templates/plugin-new/backend/tsconfig.json b/templates/plugin-new/backend/tsconfig.json index 7e1995b..2d98654 100644 --- a/templates/plugin-new/backend/tsconfig.json +++ b/templates/plugin-new/backend/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@backstage/cli/config/tsconfig.json", - "include": ["src"], + "include": ["src", "dev"], "compilerOptions": { "outDir": "dist-types", "rootDir": "." diff --git a/templates/plugin-new/frontend/dev/index.tsx b/templates/plugin-new/frontend/dev/index.tsx new file mode 100644 index 0000000..21f8429 --- /dev/null +++ b/templates/plugin-new/frontend/dev/index.tsx @@ -0,0 +1,5 @@ +import { createDevApp } from '@backstage/frontend-dev-utils'; + +import plugin from '../src'; + +createDevApp({ features: [plugin] }); diff --git a/templates/plugin-new/frontend/package.json.hbs b/templates/plugin-new/frontend/package.json.hbs index 48ec269..7143035 100644 --- a/templates/plugin-new/frontend/package.json.hbs +++ b/templates/plugin-new/frontend/package.json.hbs @@ -4,10 +4,26 @@ "packageManager": "yarn@4.17.1", "private": true, "license": "Apache-2.0", + "files": ["dist"], "main": "src/index.ts", "types": "src/index.ts", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "package.json": ["package.json"] + } + }, "backstage": { "role": "frontend-plugin", "pluginId": "{{pluginId}}" }, "scripts": { + "start": "backstage-cli package start", "build": "yarn tsc && backstage-cli package build", "tsc": "tsc --declaration", "test": "backstage-cli package test --passWithNoTests" @@ -18,6 +34,7 @@ }, "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}", + "@backstage/frontend-dev-utils": "{{versionQuery '@backstage/frontend-dev-utils'}}", "@types/jest": "^29.5.14", "@types/react": "^18.0.0", "react": "^18.0.0", diff --git a/templates/plugin-new/frontend/src/PluginPage.tsx.hbs b/templates/plugin-new/frontend/src/PluginPage.tsx.hbs index ad388ee..0da35eb 100644 --- a/templates/plugin-new/frontend/src/PluginPage.tsx.hbs +++ b/templates/plugin-new/frontend/src/PluginPage.tsx.hbs @@ -1,5 +1,3 @@ -import React from 'react'; - export function PluginPage() { - return React.createElement('h1', undefined, '{{pluginId}}'); + return

{{pluginId}}

; } diff --git a/templates/plugin-new/frontend/src/index.ts.hbs b/templates/plugin-new/frontend/src/index.ts.hbs index be474c2..2f53769 100644 --- a/templates/plugin-new/frontend/src/index.ts.hbs +++ b/templates/plugin-new/frontend/src/index.ts.hbs @@ -1,19 +1 @@ -import { createRouteRef } from '@backstage/core-plugin-api'; -import { createFrontendPlugin, PageBlueprint } from '@backstage/frontend-plugin-api'; -import React from 'react'; - -const rootRouteRef = createRouteRef({ id: '{{pluginId}}' }); -const page = PageBlueprint.make({ - params: { - title: '{{pluginId}}', - path: '/{{pluginId}}', - routeRef: rootRouteRef, - loader: () => import('./PluginPage').then(m => React.createElement(m.PluginPage)), - }, -}); - -export default createFrontendPlugin({ - pluginId: '{{pluginId}}', - extensions: [page], - routes: { root: rootRouteRef }, -}); +export { plugin as default } from './plugin'; diff --git a/templates/plugin-new/frontend/src/plugin.tsx.hbs b/templates/plugin-new/frontend/src/plugin.tsx.hbs new file mode 100644 index 0000000..1da5a55 --- /dev/null +++ b/templates/plugin-new/frontend/src/plugin.tsx.hbs @@ -0,0 +1,18 @@ +import { createRouteRef } from '@backstage/core-plugin-api'; +import { createFrontendPlugin, PageBlueprint } from '@backstage/frontend-plugin-api'; + +const rootRouteRef = createRouteRef({ id: '{{pluginId}}' }); +const page = PageBlueprint.make({ + params: { + title: '{{pluginId}}', + path: '/{{pluginId}}', + routeRef: rootRouteRef, + loader: () => import('./PluginPage').then(m => ), + }, +}); + +export const plugin = createFrontendPlugin({ + pluginId: '{{pluginId}}', + extensions: [page], + routes: { root: rootRouteRef }, +}); diff --git a/templates/plugin-new/frontend/tsconfig.json b/templates/plugin-new/frontend/tsconfig.json index 7e1995b..b7433db 100644 --- a/templates/plugin-new/frontend/tsconfig.json +++ b/templates/plugin-new/frontend/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "@backstage/cli/config/tsconfig.json", - "include": ["src"], + "include": ["src", "dev"], "compilerOptions": { + "jsx": "react-jsx", "outDir": "dist-types", "rootDir": "." }