Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <plugin-name>` as an alternative to the positional name, and `--output <directory>` 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 <plugin-name>` as an alternative to the positional name, and `--output <directory>` 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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand Down
75 changes: 58 additions & 17 deletions src/commands/new/__snapshots__/command.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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();
",
],
]
Expand Down Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -338,46 +367,58 @@ 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({
params: {
title: 'example-plugin',
path: '/example-plugin',
routeRef: rootRouteRef,
loader: () => import('./PluginPage').then(m => React.createElement(m.PluginPage)),
loader: () => import('./PluginPage').then(m => <m.PluginPage />),
},
});

export default createFrontendPlugin({
export const plugin = createFrontendPlugin({
pluginId: 'example-plugin',
extensions: [page],
routes: { root: rootRouteRef },
});
",
],
[
"tsconfig.json",
"{
"extends": "@backstage/cli/config/tsconfig.json",
"include": ["src"],
"compilerOptions": {
"outDir": "dist-types",
"rootDir": "."
}
"src/PluginPage.tsx",
"export function PluginPage() {
return <h1>example-plugin</h1>;
}
",
],
[
"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] });
",
],
]
Expand Down
19 changes: 16 additions & 3 deletions src/commands/new/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ describe('createPluginProject', () => {
backstageVersion: '1.54.0',
source: 'matrix',
packages: new Map([
['@backstage/backend-defaults', '0.12.5'],
Comment thread
gashcrumb marked this conversation as resolved.
['@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'],
]),
Expand All @@ -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',
Expand Down Expand Up @@ -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');
Expand All @@ -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'),
Expand Down
6 changes: 6 additions & 0 deletions templates/plugin-new/backend/dev/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();

backend.add(import('../src'));
backend.start();
2 changes: 2 additions & 0 deletions templates/plugin-new/backend/package.json.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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'}}",
Comment thread
gashcrumb marked this conversation as resolved.
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
Expand Down
2 changes: 1 addition & 1 deletion templates/plugin-new/backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": ["src"],
"include": ["src", "dev"],
"compilerOptions": {
"outDir": "dist-types",
"rootDir": "."
Expand Down
5 changes: 5 additions & 0 deletions templates/plugin-new/frontend/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createDevApp } from '@backstage/frontend-dev-utils';

import plugin from '../src';

createDevApp({ features: [plugin] });
17 changes: 17 additions & 0 deletions templates/plugin-new/frontend/package.json.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,26 @@
"packageManager": "yarn@4.17.1",
"private": true,
"license": "Apache-2.0",
"files": ["dist"],
Comment thread
gashcrumb marked this conversation as resolved.
Comment thread
gashcrumb marked this conversation as resolved.
"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"
Expand All @@ -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",
Expand Down
4 changes: 1 addition & 3 deletions templates/plugin-new/frontend/src/PluginPage.tsx.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import React from 'react';

export function PluginPage() {
return React.createElement('h1', undefined, '{{pluginId}}');
return <h1>{{pluginId}}</h1>;
}
20 changes: 1 addition & 19 deletions templates/plugin-new/frontend/src/index.ts.hbs
Original file line number Diff line number Diff line change
@@ -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';
18 changes: 18 additions & 0 deletions templates/plugin-new/frontend/src/plugin.tsx.hbs
Original file line number Diff line number Diff line change
@@ -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 => <m.PluginPage />),
},
});

export const plugin = createFrontendPlugin({
Comment thread
gashcrumb marked this conversation as resolved.
pluginId: '{{pluginId}}',
extensions: [page],
routes: { root: rootRouteRef },
});
3 changes: 2 additions & 1 deletion templates/plugin-new/frontend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": ["src"],
"include": ["src", "dev"],
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "dist-types",
"rootDir": "."
}
Expand Down
Loading