Move yarn new templates from @backstage/cli to @backstage/cli-module-new
The built-in templates used by `yarn new` are moved from `packages/cli/templates/` to `packages/cli-module-new/templates/`, colocating them with the code that consumes them. A backwards compatibility rewrite in the template resolution ensures that existing `@backstage/cli/templates/*` references in root `package.json` configurations continue to work by transparently resolving them to `@backstage/cli-module-new/templates/*`. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -19,9 +19,11 @@
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"bin": "bin/backstage-cli-module-new",
|
||||
"files": [
|
||||
"dist",
|
||||
"bin"
|
||||
"bin",
|
||||
"templates"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
@@ -57,6 +59,5 @@
|
||||
"@types/inquirer": "^8.1.3",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"@types/recursive-readdir": "^2.2.0"
|
||||
},
|
||||
"bin": "bin/backstage-cli-module-new"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,15 +15,15 @@
|
||||
*/
|
||||
|
||||
export const defaultTemplates = [
|
||||
'@backstage/cli/templates/frontend-plugin',
|
||||
'@backstage/cli/templates/backend-plugin',
|
||||
'@backstage/cli/templates/backend-plugin-module',
|
||||
'@backstage/cli/templates/plugin-web-library',
|
||||
'@backstage/cli/templates/plugin-node-library',
|
||||
'@backstage/cli/templates/plugin-common-library',
|
||||
'@backstage/cli/templates/web-library',
|
||||
'@backstage/cli/templates/node-library',
|
||||
'@backstage/cli/templates/cli-module',
|
||||
'@backstage/cli/templates/catalog-provider-module',
|
||||
'@backstage/cli/templates/scaffolder-backend-module',
|
||||
'@backstage/cli-module-new/templates/frontend-plugin',
|
||||
'@backstage/cli-module-new/templates/backend-plugin',
|
||||
'@backstage/cli-module-new/templates/backend-plugin-module',
|
||||
'@backstage/cli-module-new/templates/plugin-web-library',
|
||||
'@backstage/cli-module-new/templates/plugin-node-library',
|
||||
'@backstage/cli-module-new/templates/plugin-common-library',
|
||||
'@backstage/cli-module-new/templates/web-library',
|
||||
'@backstage/cli-module-new/templates/node-library',
|
||||
'@backstage/cli-module-new/templates/cli-module',
|
||||
'@backstage/cli-module-new/templates/catalog-provider-module',
|
||||
'@backstage/cli-module-new/templates/scaffolder-backend-module',
|
||||
];
|
||||
|
||||
@@ -200,6 +200,72 @@ describe('loadPortableTemplateConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should rewrite legacy @backstage/cli/templates paths', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
backstage: {
|
||||
cli: {
|
||||
new: {
|
||||
templates: [
|
||||
'@backstage/cli/templates/backend-plugin',
|
||||
'@backstage/cli/templates/frontend-plugin',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
node_modules: {
|
||||
'@backstage': {
|
||||
'cli-module-new': {
|
||||
templates: {
|
||||
'backend-plugin': {
|
||||
[TEMPLATE_FILE_NAME]:
|
||||
'name: backend-plugin\nrole: backend-plugin\n',
|
||||
},
|
||||
'frontend-plugin': {
|
||||
[TEMPLATE_FILE_NAME]:
|
||||
'name: frontend-plugin\nrole: frontend-plugin\n',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
loadPortableTemplateConfig({
|
||||
packagePath: mockDir.resolve('package.json'),
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
isUsingDefaultTemplates: false,
|
||||
templatePointers: [
|
||||
{
|
||||
name: 'backend-plugin',
|
||||
target: realpathSync(
|
||||
mockDir.resolve(
|
||||
'node_modules/@backstage/cli-module-new/templates/backend-plugin',
|
||||
TEMPLATE_FILE_NAME,
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'frontend-plugin',
|
||||
target: realpathSync(
|
||||
mockDir.resolve(
|
||||
'node_modules/@backstage/cli-module-new/templates/frontend-plugin',
|
||||
TEMPLATE_FILE_NAME,
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
license: 'Apache-2.0',
|
||||
private: true,
|
||||
version: '0.1.0',
|
||||
packageNamePrefix: '@internal/',
|
||||
packageNamePluginInfix: 'backstage-plugin-',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject templates with conflicting names', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
|
||||
@@ -157,6 +157,9 @@ export async function loadPortableTemplateConfig(
|
||||
};
|
||||
}
|
||||
|
||||
const CLI_TEMPLATE_PREFIX = '@backstage/cli/templates/';
|
||||
const CLI_MODULE_NEW_TEMPLATE_PREFIX = '@backstage/cli-module-new/templates/';
|
||||
|
||||
function resolveLocalTemplatePath(pointer: string, basePath: string): string {
|
||||
if (isAbsolute(pointer)) {
|
||||
throw new Error(`Template target may not be an absolute path`);
|
||||
@@ -166,7 +169,14 @@ function resolveLocalTemplatePath(pointer: string, basePath: string): string {
|
||||
return resolvePath(basePath, pointer, TEMPLATE_FILE_NAME);
|
||||
}
|
||||
|
||||
return require.resolve(`${pointer}/${TEMPLATE_FILE_NAME}`, {
|
||||
// Rewrite legacy @backstage/cli/templates/* paths to @backstage/cli-module-new/templates/*
|
||||
const resolvedPointer = pointer.startsWith(CLI_TEMPLATE_PREFIX)
|
||||
? `${CLI_MODULE_NEW_TEMPLATE_PREFIX}${pointer.slice(
|
||||
CLI_TEMPLATE_PREFIX.length,
|
||||
)}`
|
||||
: pointer;
|
||||
|
||||
return require.resolve(`${resolvedPointer}/${TEMPLATE_FILE_NAME}`, {
|
||||
paths: [basePath],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{packageName}}
|
||||
|
||||
The {{moduleId}} backend module for the {{pluginId}} plugin.
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"description": "The {{moduleId}} backend module for the {{pluginId}} plugin.",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module",
|
||||
"pluginId": "{{pluginId}}"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}",
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
name: backend-plugin-module
|
||||
role: backend-plugin-module
|
||||
description: A new backend module that extends an existing backend plugin
|
||||
values:
|
||||
moduleVar: '{{ camelCase pluginId }}Module{{ upperFirst ( camelCase moduleId ) }}'
|
||||
@@ -0,0 +1,8 @@
|
||||
/***/
|
||||
/**
|
||||
* The {{moduleId}} backend module for the {{pluginId}} plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { {{moduleVar}} as default } from './module';
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
export const {{moduleVar}} = createBackendModule({
|
||||
pluginId: '{{pluginId}}',
|
||||
moduleId: '{{moduleId}}',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: { logger: coreServices.logger },
|
||||
async init({ logger }) {
|
||||
logger.info('Hello World!');
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,28 @@
|
||||
# {{pluginId}}
|
||||
|
||||
This plugin backend was templated using the Backstage CLI. You should replace this text with a description of your plugin backend.
|
||||
|
||||
## Installation
|
||||
|
||||
This plugin is installed via the `{{packageName}}` package. To install it to your backend package, run the following command:
|
||||
|
||||
```bash
|
||||
# From your root directory
|
||||
yarn --cwd packages/backend add {{packageName}}
|
||||
```
|
||||
|
||||
Then add the plugin to your backend in `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
const backend = createBackend();
|
||||
// ...
|
||||
backend.add(import('{{packageName}}'));
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
This plugin backend can be started in a standalone mode from directly in this
|
||||
package with `yarn start`. It is a limited setup that is most convenient when
|
||||
developing the plugin backend itself.
|
||||
|
||||
If you want to run the entire project, including the frontend, run `yarn start` from the root directory.
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// This is the development setup for your plugin that wires up a
|
||||
// minimal backend that can use both real and mocked plugins and services.
|
||||
//
|
||||
// Start up the backend by running `yarn start` in the package directory.
|
||||
// Once it's up and running, try out the following requests:
|
||||
//
|
||||
// Create a new todo item, standalone or for the sample component:
|
||||
//
|
||||
// curl http://localhost:7007/api/{{pluginId}}/todos -H 'Content-Type: application/json' -d '{"title": "My Todo"}'
|
||||
// curl http://localhost:7007/api/{{pluginId}}/todos -H 'Content-Type: application/json' -d '{"title": "My Todo", "entityRef": "component:default/sample"}'
|
||||
//
|
||||
// List TODOs:
|
||||
//
|
||||
// curl http://localhost:7007/api/{{pluginId}}/todos
|
||||
//
|
||||
// Explicitly make an unauthenticated request, or with service auth:
|
||||
//
|
||||
// curl http://localhost:7007/api/{{pluginId}}/todos -H 'Authorization: Bearer mock-none-token'
|
||||
// curl http://localhost:7007/api/{{pluginId}}/todos -H 'Authorization: Bearer mock-service-token'
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Mocking the auth and httpAuth service allows you to call your plugin API without
|
||||
// having to authenticate.
|
||||
//
|
||||
// If you want to use real auth, you can install the following instead:
|
||||
// backend.add(import('@backstage/plugin-auth-backend'));
|
||||
// backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
|
||||
backend.add(mockServices.auth.factory());
|
||||
backend.add(mockServices.httpAuth.factory());
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Rather than using a real catalog you can use a mock with a fixed set of entities.
|
||||
backend.add(
|
||||
catalogServiceMock.factory({
|
||||
entities: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'sample',
|
||||
title: 'Sample Component',
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
backend.add(import('../src'));
|
||||
|
||||
backend.start();
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "backend-plugin",
|
||||
"pluginId": "{{pluginId}}"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-defaults": "{{versionQuery '@backstage/backend-defaults'}}",
|
||||
"@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}",
|
||||
"@backstage/catalog-client": "{{versionQuery '@backstage/catalog-client'}}",
|
||||
"@backstage/errors": "{{versionQuery '@backstage/errors'}}",
|
||||
"@backstage/types": "{{versionQuery '@backstage/types'}}",
|
||||
"@backstage/plugin-catalog-node": "{{versionQuery '@backstage/plugin-catalog-node'}}",
|
||||
"express": "{{versionQuery 'express' '4.17.1'}}",
|
||||
"express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}",
|
||||
"zod": "{{versionQuery 'zod' '3.25.76'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}",
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@types/express": "{{versionQuery '@types/express' '4.17.6'}}",
|
||||
"@types/supertest": "{{versionQuery '@types/supertest' '2.0.12'}}",
|
||||
"supertest": "{{versionQuery 'supertest' '6.2.4'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
name: backend-plugin
|
||||
role: backend-plugin
|
||||
description: A new backend plugin
|
||||
values:
|
||||
pluginVar: '{{ camelCase pluginId }}Plugin'
|
||||
@@ -0,0 +1 @@
|
||||
export { {{pluginVar}} as default } from './plugin';
|
||||
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
mockCredentials,
|
||||
startTestBackend,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { createServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { todoListServiceRef } from './services/TodoListService';
|
||||
import { {{pluginVar}} } from './plugin';
|
||||
import request from 'supertest';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
import {
|
||||
ConflictError,
|
||||
AuthenticationError,
|
||||
NotAllowedError,
|
||||
} from '@backstage/errors';
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Plugin tests are integration tests for your plugin, ensuring that all pieces
|
||||
// work together end-to-end. You can still mock injected backend services
|
||||
// however, just like anyone who installs your plugin might replace the
|
||||
// services with their own implementations.
|
||||
describe('plugin', () => {
|
||||
it('should create and read TODO items', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [{{pluginVar}}],
|
||||
});
|
||||
|
||||
await request(server).get('/api/{{pluginId}}/todos').expect(200, {
|
||||
items: [],
|
||||
});
|
||||
|
||||
const createRes = await request(server)
|
||||
.post('/api/{{pluginId}}/todos')
|
||||
.send({ title: 'My Todo' });
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
expect(createRes.body).toEqual({
|
||||
id: expect.any(String),
|
||||
title: 'My Todo',
|
||||
createdBy: mockCredentials.user().principal.userEntityRef,
|
||||
createdAt: expect.any(String),
|
||||
});
|
||||
|
||||
const createdTodoItem = createRes.body;
|
||||
|
||||
await request(server)
|
||||
.get('/api/{{pluginId}}/todos')
|
||||
.expect(200, {
|
||||
items: [createdTodoItem],
|
||||
});
|
||||
|
||||
await request(server)
|
||||
.get(`/api/{{pluginId}}/todos/${createdTodoItem.id}`)
|
||||
.expect(200, createdTodoItem);
|
||||
});
|
||||
|
||||
it('should create TODO item with catalog information', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
{{pluginVar}},
|
||||
catalogServiceMock.factory({
|
||||
entities: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'my-component',
|
||||
namespace: 'default',
|
||||
title: 'My Component',
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
owner: 'me',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const createRes = await request(server)
|
||||
.post('/api/{{pluginId}}/todos')
|
||||
.send({ title: 'My Todo', entityRef: 'component:default/my-component' });
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
expect(createRes.body).toEqual({
|
||||
id: expect.any(String),
|
||||
title: '[My Component] My Todo',
|
||||
createdBy: mockCredentials.user().principal.userEntityRef,
|
||||
createdAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should forward errors from the TodoListService', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
{{pluginVar}},
|
||||
createServiceFactory({
|
||||
service: todoListServiceRef,
|
||||
deps: {},
|
||||
factory: () => ({
|
||||
createTodo: jest.fn().mockRejectedValue(new ConflictError()),
|
||||
listTodos: jest.fn().mockRejectedValue(new AuthenticationError()),
|
||||
getTodo: jest.fn().mockRejectedValue(new NotAllowedError()),
|
||||
}),
|
||||
})
|
||||
],
|
||||
});
|
||||
|
||||
const createRes = await request(server)
|
||||
.post('/api/{{pluginId}}/todos')
|
||||
.send({ title: 'My Todo', entityRef: 'component:default/my-component' });
|
||||
expect(createRes.status).toBe(409);
|
||||
expect(createRes.body).toMatchObject({
|
||||
error: { name: 'ConflictError' },
|
||||
});
|
||||
|
||||
const listRes = await request(server).get('/api/{{pluginId}}/todos');
|
||||
expect(listRes.status).toBe(401);
|
||||
expect(listRes.body).toMatchObject({
|
||||
error: { name: 'AuthenticationError' },
|
||||
});
|
||||
|
||||
const getRes = await request(server).get('/api/{{pluginId}}/todos/123');
|
||||
expect(getRes.status).toBe(403);
|
||||
expect(getRes.body).toMatchObject({
|
||||
error: { name: 'NotAllowedError' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createRouter } from './router';
|
||||
import { todoListServiceRef } from './services/TodoListService';
|
||||
|
||||
/**
|
||||
* {{pluginVar}} backend plugin
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const {{pluginVar}} = createBackendPlugin({
|
||||
pluginId: '{{pluginId}}',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
httpAuth: coreServices.httpAuth,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
todoList: todoListServiceRef,
|
||||
},
|
||||
async init({ httpAuth, httpRouter, todoList }) {
|
||||
httpRouter.use(
|
||||
await createRouter({
|
||||
httpAuth,
|
||||
todoList,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
mockCredentials,
|
||||
mockErrorHandler,
|
||||
mockServices,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
import { createRouter } from './router';
|
||||
import { todoListServiceRef } from './services/TodoListService';
|
||||
|
||||
const mockTodoItem = {
|
||||
title: 'Do the thing',
|
||||
id: '123',
|
||||
createdBy: mockCredentials.user().principal.userEntityRef,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Testing the router directly allows you to write a unit test that mocks the provided options.
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
let todoList: jest.Mocked<typeof todoListServiceRef.T>;
|
||||
|
||||
beforeEach(async () => {
|
||||
todoList = {
|
||||
createTodo: jest.fn(),
|
||||
listTodos: jest.fn(),
|
||||
getTodo: jest.fn(),
|
||||
};
|
||||
const router = await createRouter({
|
||||
httpAuth: mockServices.httpAuth(),
|
||||
todoList,
|
||||
});
|
||||
app = express();
|
||||
app.use(router);
|
||||
app.use(mockErrorHandler());
|
||||
});
|
||||
|
||||
it('should create a TODO', async () => {
|
||||
todoList.createTodo.mockResolvedValue(mockTodoItem);
|
||||
|
||||
const response = await request(app).post('/todos').send({
|
||||
title: 'Do the thing',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toEqual(mockTodoItem);
|
||||
});
|
||||
|
||||
it('should not allow unauthenticated requests to create a TODO', async () => {
|
||||
todoList.createTodo.mockResolvedValue(mockTodoItem);
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// The HttpAuth mock service considers all requests to be authenticated as a
|
||||
// mock user by default. In order to test other cases we need to explicitly
|
||||
// pass an authorization header with mock credentials.
|
||||
const response = await request(app)
|
||||
.post('/todos')
|
||||
.set('Authorization', mockCredentials.none.header())
|
||||
.send({
|
||||
title: 'Do the thing',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { HttpAuthService } from '@backstage/backend-plugin-api';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { z } from 'zod';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { todoListServiceRef } from './services/TodoListService';
|
||||
|
||||
export async function createRouter({
|
||||
httpAuth,
|
||||
todoList,
|
||||
}: {
|
||||
httpAuth: HttpAuthService;
|
||||
todoList: typeof todoListServiceRef.T;
|
||||
}): Promise<express.Router> {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Zod is a powerful library for data validation and recommended in particular
|
||||
// for user-defined schemas. In this case we use it for input validation too.
|
||||
//
|
||||
// If you want to define a schema for your API we recommend using Backstage's
|
||||
// OpenAPI tooling: https://backstage.io/docs/next/openapi/01-getting-started
|
||||
const todoSchema = z.object({
|
||||
title: z.string(),
|
||||
entityRef: z.string().optional(),
|
||||
});
|
||||
|
||||
router.post('/todos', async (req, res) => {
|
||||
const parsed = todoSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
throw new InputError(parsed.error.toString());
|
||||
}
|
||||
|
||||
const result = await todoList.createTodo(parsed.data, {
|
||||
credentials: await httpAuth.credentials(req, { allow: ['user'] }),
|
||||
});
|
||||
|
||||
res.status(201).json(result);
|
||||
});
|
||||
|
||||
router.get('/todos', async (_req, res) => {
|
||||
res.json(await todoList.listTodos());
|
||||
});
|
||||
|
||||
router.get('/todos/:id', async (req, res) => {
|
||||
res.json(await todoList.getTodo({ id: req.params.id }));
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import crypto from 'node:crypto';
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
createServiceRef,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
|
||||
import {
|
||||
BackstageCredentials,
|
||||
BackstageUserPrincipal,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Expand } from '@backstage/types';
|
||||
|
||||
export interface TodoItem {
|
||||
title: string;
|
||||
id: string;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// This is a simple in-memory todo list store. It is recommended to use a
|
||||
// database to store data in a real application. See the database service
|
||||
// documentation for more information on how to do this:
|
||||
// https://backstage.io/docs/backend-system/core-services/database
|
||||
export class TodoListService {
|
||||
readonly #logger: LoggerService;
|
||||
readonly #catalog: typeof catalogServiceRef.T;
|
||||
|
||||
readonly #storedTodos = new Array<TodoItem>();
|
||||
|
||||
static create(options: {
|
||||
logger: LoggerService;
|
||||
catalog: typeof catalogServiceRef.T;
|
||||
}) {
|
||||
return new TodoListService(options.logger, options.catalog);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
logger: LoggerService,
|
||||
catalog: typeof catalogServiceRef.T,
|
||||
) {
|
||||
this.#logger = logger;
|
||||
this.#catalog = catalog;
|
||||
}
|
||||
|
||||
async createTodo(
|
||||
input: {
|
||||
title: string;
|
||||
entityRef?: string;
|
||||
},
|
||||
options: {
|
||||
credentials: BackstageCredentials<BackstageUserPrincipal>;
|
||||
},
|
||||
): Promise<TodoItem> {
|
||||
let title = input.title;
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// A common pattern for Backstage plugins is to pass an entity reference
|
||||
// from the frontend to then fetch the entire entity from the catalog in the
|
||||
// backend plugin.
|
||||
if (input.entityRef) {
|
||||
// TEMPLATE NOTE:
|
||||
// Cross-plugin communication uses service-to-service authentication. The
|
||||
// `AuthService` lets you generate a token that is valid for communication
|
||||
// with the target plugin only. You must also provide credentials for the
|
||||
// identity that you are making the request on behalf of.
|
||||
//
|
||||
// If you want to make a request using the plugin backend's own identity,
|
||||
// you can access it via the `auth.getOwnServiceCredentials()` method.
|
||||
// Beware that this bypasses any user permission checks.
|
||||
const entity = await this.#catalog.getEntityByRef(
|
||||
input.entityRef,
|
||||
options,
|
||||
);
|
||||
if (!entity) {
|
||||
throw new NotFoundError(`No entity found for ref '${input.entityRef}'`);
|
||||
}
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// Here you could read any form of data from the entity. A common use case
|
||||
// is to read the value of a custom annotation for your plugin. You can
|
||||
// read more about how to add custom annotations here:
|
||||
// https://backstage.io/docs/features/software-catalog/extending-the-model#adding-a-new-annotation
|
||||
//
|
||||
// In this example we just use the entity title to decorate the todo item.
|
||||
|
||||
const entityDisplay = entity.metadata.title ?? input.entityRef;
|
||||
title = `[${entityDisplay}] ${input.title}`;
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const createdBy = options.credentials.principal.userEntityRef;
|
||||
const newTodo = {
|
||||
title,
|
||||
id,
|
||||
createdBy,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.#storedTodos.push(newTodo);
|
||||
|
||||
// TEMPLATE NOTE:
|
||||
// The second argument of the logger methods can be used to pass
|
||||
// structured metadata. You can read more about the logger service here:
|
||||
// https://backstage.io/docs/backend-system/core-services/logger
|
||||
this.#logger.info('Created new todo item', { id, title, createdBy });
|
||||
|
||||
return newTodo;
|
||||
}
|
||||
|
||||
async listTodos(): Promise<{ items: TodoItem[] }> {
|
||||
return { items: Array.from(this.#storedTodos) };
|
||||
}
|
||||
|
||||
async getTodo(request: { id: string }): Promise<TodoItem> {
|
||||
const todo = this.#storedTodos.find(item => item.id === request.id);
|
||||
if (!todo) {
|
||||
throw new NotFoundError(`No todo found with id '${request.id}'`);
|
||||
}
|
||||
return todo;
|
||||
}
|
||||
}
|
||||
|
||||
export const todoListServiceRef = createServiceRef<Expand<TodoListService>>({
|
||||
id: 'todo.list',
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {
|
||||
logger: coreServices.logger,
|
||||
catalog: catalogServiceRef,
|
||||
},
|
||||
async factory(deps) {
|
||||
return TodoListService.create(deps);
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{packageName}}
|
||||
|
||||
The {{fullModuleId}} module for [@backstage/plugin-catalog-backend](https://www.npmjs.com/package/@backstage/plugin-catalog-backend).
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,34 @@
|
||||
import { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api';
|
||||
|
||||
export interface Config {
|
||||
catalog?: {
|
||||
providers?: {
|
||||
/**
|
||||
* {{providerClass}} configuration.
|
||||
*/
|
||||
{{providerVar}}?:
|
||||
| {
|
||||
/**
|
||||
* The target that this provider should consume.
|
||||
*/
|
||||
target: string;
|
||||
/**
|
||||
* Overrides the schedule at which this provider runs.
|
||||
*/
|
||||
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
|
||||
}
|
||||
| {
|
||||
[name: string]: {
|
||||
/**
|
||||
* The target that this provider should consume.
|
||||
*/
|
||||
target: string;
|
||||
/**
|
||||
* Overrides the schedule at which this provider runs.
|
||||
*/
|
||||
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"description": "The {{fullModuleId}} module for @backstage/plugin-catalog-backend",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module",
|
||||
"pluginId": "catalog",
|
||||
"pluginPackage": "@backstage/plugin-catalog-backend"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}",
|
||||
"@backstage/plugin-catalog-node": "{{versionQuery '@backstage/plugin-catalog-node'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
name: catalog-provider-module
|
||||
role: backend-plugin-module
|
||||
description: An Entity Provider module for the Software Catalog
|
||||
values:
|
||||
pluginId: catalog
|
||||
fullModuleId: '{{ moduleId }}-provider'
|
||||
moduleVar: '{{ camelCase pluginId }}Module{{ upperFirst ( camelCase moduleId ) }}'
|
||||
providerVar: '{{ camelCase moduleId }}Provider'
|
||||
providerClass: '{{ upperFirst ( camelCase moduleId ) }}Provider'
|
||||
@@ -0,0 +1,8 @@
|
||||
/***/
|
||||
/**
|
||||
* The {{fullModuleId}} module for @backstage/plugin-catalog-backend
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { {{moduleVar}} as default } from './module';
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
|
||||
import { {{providerClass}} } from './provider/{{providerClass}}';
|
||||
|
||||
export const {{moduleVar}} = createBackendModule({
|
||||
moduleId: '{{fullModuleId}}',
|
||||
pluginId: '{{pluginId}}',
|
||||
register({ registerInit }) {
|
||||
registerInit({
|
||||
deps: {
|
||||
logger: coreServices.logger,
|
||||
config: coreServices.rootConfig,
|
||||
scheduler: coreServices.scheduler,
|
||||
processing: catalogProcessingExtensionPoint,
|
||||
},
|
||||
async init({ logger, scheduler, config, processing }) {
|
||||
processing.addEntityProvider(
|
||||
{{providerClass}}.fromConfig(config, {
|
||||
logger,
|
||||
scheduler,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
readSchedulerServiceTaskScheduleDefinitionFromConfig,
|
||||
SchedulerServiceTaskScheduleDefinition,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
const DEFAULT_PROVIDER_ID = 'default';
|
||||
const DEFAULT_SCHEDULE: SchedulerServiceTaskScheduleDefinition = {
|
||||
frequency: {
|
||||
minutes: 30,
|
||||
},
|
||||
timeout: {
|
||||
minutes: 3,
|
||||
},
|
||||
}
|
||||
|
||||
export type {{providerClass}}ProviderConfig = {
|
||||
id: string;
|
||||
target: string;
|
||||
schedule: SchedulerServiceTaskScheduleDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses all configured providers.
|
||||
*
|
||||
* @param config - The root of the provider config hierarchy
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function readProviderConfigs(
|
||||
config: Config,
|
||||
): {{providerClass}}ProviderConfig[] {
|
||||
const providersConfig = config.getOptionalConfig(
|
||||
'catalog.providers.{{providerVar}}',
|
||||
);
|
||||
if (!providersConfig) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ((providersConfig).has('target')) {
|
||||
// simple/single config variant
|
||||
return [readProviderConfig(DEFAULT_PROVIDER_ID, providersConfig)];
|
||||
}
|
||||
|
||||
return providersConfig.keys().map(id => {
|
||||
const providerConfig = providersConfig.getConfig(id);
|
||||
|
||||
return readProviderConfig(id, providerConfig);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single configured provider by id.
|
||||
*
|
||||
* @param id - the id of the provider to parse
|
||||
* @param config - The root of the provider config hierarchy
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function readProviderConfig(
|
||||
id: string,
|
||||
config: Config,
|
||||
): {{providerClass}}ProviderConfig {
|
||||
|
||||
const target = config.getString('target');
|
||||
|
||||
const schedule = config.has('schedule')
|
||||
? readSchedulerServiceTaskScheduleDefinitionFromConfig(
|
||||
config.getConfig('schedule'),
|
||||
)
|
||||
: DEFAULT_SCHEDULE;
|
||||
|
||||
return {
|
||||
id,
|
||||
target,
|
||||
schedule,
|
||||
};
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { {{providerClass}} } from './{{providerClass}}';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('{{providerClass}}', () => {
|
||||
it('should read entities from the target', async () => {
|
||||
const logger = mockServices.logger.mock();
|
||||
const provider = new {{providerClass}}({
|
||||
id: 'test',
|
||||
target: 'https://example.com',
|
||||
logger: mockServices.logger.mock(),
|
||||
taskRunner: { run: jest.fn() },
|
||||
});
|
||||
|
||||
const entities = await provider.read({ logger });
|
||||
|
||||
expect(entities).toEqual([]);
|
||||
});
|
||||
})
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
DeferredEntity,
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import * as uuid from 'uuid';
|
||||
import { readProviderConfigs } from './readProviderConfigs';
|
||||
import {
|
||||
LoggerService,
|
||||
SchedulerService,
|
||||
SchedulerServiceTaskRunner,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
export type {{providerClass}}Options = {
|
||||
/**
|
||||
* The logger to use.
|
||||
*/
|
||||
logger: LoggerService;
|
||||
|
||||
/**
|
||||
* Scheduler used to schedule refreshes based on
|
||||
* the schedule config.
|
||||
*/
|
||||
scheduler: SchedulerService;
|
||||
};
|
||||
|
||||
export class {{providerClass}} implements EntityProvider {
|
||||
static fromConfig(
|
||||
configRoot: Config,
|
||||
options: {{providerClass}}Options,
|
||||
): {{providerClass}}[] {
|
||||
return readProviderConfigs(configRoot).map(providerConfig => {
|
||||
return new {{providerClass}}({
|
||||
id: providerConfig.id,
|
||||
target: providerConfig.target,
|
||||
logger: options.logger,
|
||||
taskRunner: options.scheduler.createScheduledTaskRunner(
|
||||
providerConfig.schedule,
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
readonly #id: string;
|
||||
readonly #target: string;
|
||||
readonly #logger: LoggerService;
|
||||
readonly #taskRunner: SchedulerServiceTaskRunner;
|
||||
|
||||
constructor(options: {
|
||||
id: string;
|
||||
target: string;
|
||||
logger: LoggerService;
|
||||
taskRunner: SchedulerServiceTaskRunner;
|
||||
}) {
|
||||
this.#id = options.id;
|
||||
this.#target = options.target;
|
||||
this.#logger = options.logger;
|
||||
this.#taskRunner = options.taskRunner;
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/plugin-catalog-node#EntityProvider.getProviderName} */
|
||||
getProviderName() {
|
||||
return `{{providerClass}}:${this.#id}`;
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/plugin-catalog-node#EntityProvider.connect} */
|
||||
async connect(connection: EntityProviderConnection) {
|
||||
const id = `${this.getProviderName()}:refresh`;
|
||||
|
||||
// Schedule a refresh task to be run periodically
|
||||
await this.#taskRunner.run({
|
||||
id,
|
||||
fn: async () => {
|
||||
const logger = this.#logger.child({
|
||||
taskId: id,
|
||||
taskInstanceId: uuid.v4(),
|
||||
});
|
||||
|
||||
try {
|
||||
const entities = await this.read({ logger });
|
||||
|
||||
logger.info(`Read ${entities.length} entities`);
|
||||
|
||||
await connection.applyMutation({
|
||||
type: 'full',
|
||||
entities,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Refresh failed`, error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads entities to be added to the catalog.
|
||||
*/
|
||||
async read(options: { logger: LoggerService }): Promise<DeferredEntity[]> {
|
||||
const { logger } = options;
|
||||
|
||||
logger.info(`Reading entities from ${this.#target}`);
|
||||
|
||||
// TODO: Implement entity reading logic from the target
|
||||
const entities: DeferredEntity[] = [];
|
||||
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{packageName}}
|
||||
|
||||
A CLI module that adds commands to the Backstage CLI.
|
||||
|
||||
_This package was created through the Backstage CLI_
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
const isLocal = require('node:fs').existsSync(
|
||||
path.resolve(__dirname, '../src'),
|
||||
);
|
||||
|
||||
if (isLocal) {
|
||||
require('@backstage/cli-node/config/nodeTransform.cjs');
|
||||
}
|
||||
|
||||
const { runCliModule } = require('@backstage/cli-node');
|
||||
const cliModule = require(isLocal ? '../src/index' : '..').default;
|
||||
const pkg = require('../package.json');
|
||||
runCliModule({ module: cliModule, name: pkg.name, version: pkg.version });
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"description": "CLI module for Backstage CLI",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "cli-module"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "{{versionQuery '@backstage/cli-common'}}",
|
||||
"@backstage/cli-node": "{{versionQuery '@backstage/cli-node'}}",
|
||||
"cleye": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"bin"
|
||||
],
|
||||
"bin": "bin/{{binName}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
name: cli-module
|
||||
role: cli-module
|
||||
description: A CLI module that adds commands to the Backstage CLI
|
||||
values:
|
||||
binName: 'backstage-cli-module-{{ name }}'
|
||||
@@ -0,0 +1,22 @@
|
||||
import { cli } from 'cleye';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
const { flags } = cli(
|
||||
{
|
||||
help: info,
|
||||
booleanFlagNegation: true,
|
||||
flags: {
|
||||
name: {
|
||||
type: String,
|
||||
description: 'Your name',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const name = flags.name ?? 'World';
|
||||
console.log(`Hello, ${name}!`);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/***/
|
||||
/**
|
||||
* CLI module for the Backstage CLI.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { createCliModule } from '@backstage/cli-node';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
export default createCliModule({
|
||||
packageJson,
|
||||
init: async reg => {
|
||||
reg.addCommand({
|
||||
path: ['example'],
|
||||
description: 'An example command',
|
||||
execute: { loader: () => import('./commands/example') },
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,13 @@
|
||||
# {{pluginId}}
|
||||
|
||||
Welcome to the {{pluginId}} plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
|
||||
## Getting started
|
||||
|
||||
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{pluginId}}](http://localhost:3000/{{pluginId}}).
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { {{ pluginVar }}, {{ extensionName }} } from '../src/plugin';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin({{ pluginVar }})
|
||||
.addPage({
|
||||
element: <{{ extensionName }} />,
|
||||
title: 'Root Page',
|
||||
path: '/{{pluginId}}',
|
||||
})
|
||||
.render();
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin",
|
||||
"pluginId": "{{pluginId}}"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core-components": "{{versionQuery '@backstage/core-components'}}",
|
||||
"@backstage/core-plugin-api": "{{versionQuery '@backstage/core-plugin-api'}}",
|
||||
"@backstage/theme": "{{versionQuery '@backstage/theme'}}",
|
||||
"@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}",
|
||||
"@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}",
|
||||
"@material-ui/lab": "{{versionQuery '@material-ui/lab' '4.0.0-alpha.61'}}",
|
||||
"react-use": "{{versionQuery 'react-use' '17.2.4'}}"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
|
||||
"react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
|
||||
"react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}",
|
||||
"@backstage/dev-utils": "{{versionQuery '@backstage/dev-utils'}}",
|
||||
"@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}",
|
||||
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}",
|
||||
"@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}",
|
||||
"@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}",
|
||||
"msw": "{{versionQuery 'msw' '1.0.0'}}",
|
||||
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
|
||||
"react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}",
|
||||
"react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
name: frontend-plugin
|
||||
role: frontend-plugin
|
||||
description: A new frontend plugin
|
||||
values:
|
||||
pluginVar: '{{ camelCase pluginId }}Plugin'
|
||||
extensionName: '{{ upperFirst ( camelCase pluginId ) }}Page'
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { ExampleComponent } from './ExampleComponent';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { screen } from '@testing-library/react';
|
||||
import {
|
||||
registerMswTestHooks,
|
||||
renderInTestApp,
|
||||
} from '@backstage/test-utils';
|
||||
|
||||
describe('ExampleComponent', () => {
|
||||
const server = setupServer();
|
||||
// Enable sane handlers for network requests
|
||||
registerMswTestHooks(server);
|
||||
|
||||
// setup mock response
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render', async () => {
|
||||
await renderInTestApp(<ExampleComponent />);
|
||||
expect(
|
||||
screen.getByText('Welcome to {{pluginId}}!'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
import {
|
||||
InfoCard,
|
||||
Header,
|
||||
Page,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
import { ExampleFetchComponent } from '../ExampleFetchComponent';
|
||||
|
||||
export const ExampleComponent = () => (
|
||||
<Page themeId="tool">
|
||||
<Header title="Welcome to {{pluginId}}!" subtitle="Optional subtitle">
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Plugin title">
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard title="Information card">
|
||||
<Typography variant="body1">
|
||||
All content should be wrapped in a card like this.
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ExampleFetchComponent />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ExampleComponent } from './ExampleComponent';
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { ExampleFetchComponent } from './ExampleFetchComponent';
|
||||
|
||||
describe('ExampleFetchComponent', () => {
|
||||
it('renders the user table', async () => {
|
||||
const { getAllByText, getByAltText, getByText, findByRole } =
|
||||
await renderInTestApp(<ExampleFetchComponent />);
|
||||
|
||||
// Wait for the table to render
|
||||
const table = await findByRole('table');
|
||||
const nationality = getAllByText('GB');
|
||||
// Assert that the table contains the expected user data
|
||||
expect(table).toBeInTheDocument();
|
||||
expect(getByAltText('Carolyn')).toBeInTheDocument();
|
||||
expect(getByText('Carolyn Moore')).toBeInTheDocument();
|
||||
expect(getByText('carolyn.moore@example.com')).toBeInTheDocument();
|
||||
expect(nationality[0]).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
Table,
|
||||
TableColumn,
|
||||
Progress,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
export const exampleUsers = {
|
||||
results: [
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Miss',
|
||||
first: 'Carolyn',
|
||||
last: 'Moore',
|
||||
},
|
||||
email: 'carolyn.moore@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Carolyn',
|
||||
nat: 'GB',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Esma',
|
||||
last: 'Berberoğlu',
|
||||
},
|
||||
email: 'esma.berberoglu@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Esma',
|
||||
nat: 'TR',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Isabella',
|
||||
last: 'Rhodes',
|
||||
},
|
||||
email: 'isabella.rhodes@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella',
|
||||
nat: 'GB',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Derrick',
|
||||
last: 'Carter',
|
||||
},
|
||||
email: 'derrick.carter@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Derrick',
|
||||
nat: 'IE',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Miss',
|
||||
first: 'Mattie',
|
||||
last: 'Lambert',
|
||||
},
|
||||
email: 'mattie.lambert@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mattie',
|
||||
nat: 'AU',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Mijat',
|
||||
last: 'Rakić',
|
||||
},
|
||||
email: 'mijat.rakic@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mijat',
|
||||
nat: 'RS',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Javier',
|
||||
last: 'Reid',
|
||||
},
|
||||
email: 'javier.reid@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Javier',
|
||||
nat: 'US',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Isabella',
|
||||
last: 'Li',
|
||||
},
|
||||
email: 'isabella.li@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella',
|
||||
nat: 'CA',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Mrs',
|
||||
first: 'Stephanie',
|
||||
last: 'Garrett',
|
||||
},
|
||||
email: 'stephanie.garrett@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Stephanie',
|
||||
nat: 'AU',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Antonia',
|
||||
last: 'Núñez',
|
||||
},
|
||||
email: 'antonia.nunez@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Antonia',
|
||||
nat: 'ES',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Donald',
|
||||
last: 'Young',
|
||||
},
|
||||
email: 'donald.young@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Donald',
|
||||
nat: 'US',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Iegor',
|
||||
last: 'Holodovskiy',
|
||||
},
|
||||
email: 'iegor.holodovskiy@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Iegor',
|
||||
nat: 'UA',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Madame',
|
||||
first: 'Jessica',
|
||||
last: 'David',
|
||||
},
|
||||
email: 'jessica.david@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jessica',
|
||||
nat: 'CH',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Eve',
|
||||
last: 'Martinez',
|
||||
},
|
||||
email: 'eve.martinez@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Eve',
|
||||
nat: 'FR',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Caleb',
|
||||
last: 'Silva',
|
||||
},
|
||||
email: 'caleb.silva@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Caleb',
|
||||
nat: 'US',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Miss',
|
||||
first: 'Marcia',
|
||||
last: 'Jenkins',
|
||||
},
|
||||
email: 'marcia.jenkins@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Marcia',
|
||||
nat: 'US',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Mrs',
|
||||
first: 'Mackenzie',
|
||||
last: 'Jones',
|
||||
},
|
||||
email: 'mackenzie.jones@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mackenzie',
|
||||
nat: 'NZ',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Jeremiah',
|
||||
last: 'Gutierrez',
|
||||
},
|
||||
email: 'jeremiah.gutierrez@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jeremiah',
|
||||
nat: 'AU',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Luciara',
|
||||
last: 'Souza',
|
||||
},
|
||||
email: 'luciara.souza@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Luciara',
|
||||
nat: 'BR',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Valgi',
|
||||
last: 'da Cunha',
|
||||
},
|
||||
email: 'valgi.dacunha@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Valgi',
|
||||
nat: 'BR',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
avatar: {
|
||||
height: 32,
|
||||
width: 32,
|
||||
borderRadius: '50%',
|
||||
},
|
||||
});
|
||||
|
||||
type User = {
|
||||
gender: string; // "male"
|
||||
name: {
|
||||
title: string; // "Mr",
|
||||
first: string; // "Duane",
|
||||
last: string; // "Reed"
|
||||
};
|
||||
email: string; // "duane.reed@example.com"
|
||||
picture: string; // "https://api.dicebear.com/6.x/open-peeps/svg?seed=Duane"
|
||||
nat: string; // "AU"
|
||||
};
|
||||
|
||||
type DenseTableProps = {
|
||||
users: User[];
|
||||
};
|
||||
|
||||
export const DenseTable = ({ users }: DenseTableProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ title: 'Avatar', field: 'avatar' },
|
||||
{ title: 'Name', field: 'name' },
|
||||
{ title: 'Email', field: 'email' },
|
||||
{ title: 'Nationality', field: 'nationality' },
|
||||
];
|
||||
|
||||
const data = users.map(user => {
|
||||
return {
|
||||
avatar: (
|
||||
<img
|
||||
src={user.picture}
|
||||
className={classes.avatar}
|
||||
alt={user.name.first}
|
||||
/>
|
||||
),
|
||||
name: `${user.name.first} ${user.name.last}`,
|
||||
email: user.email,
|
||||
nationality: user.nat,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="Example User List"
|
||||
options=\{{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExampleFetchComponent = () => {
|
||||
|
||||
const { value, loading, error } = useAsync(async (): Promise<User[]> => {
|
||||
// Would use fetch in a real world example
|
||||
return exampleUsers.results;
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <ResponseErrorPanel error={error} />;
|
||||
}
|
||||
|
||||
return <DenseTable users={value || []} />;
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ExampleFetchComponent } from './ExampleFetchComponent';
|
||||
@@ -0,0 +1 @@
|
||||
export { {{ pluginVar }}, {{ extensionName }} } from './plugin';
|
||||
@@ -0,0 +1,7 @@
|
||||
import { {{ pluginVar }} } from './plugin';
|
||||
|
||||
describe('{{pluginId}}', () => {
|
||||
it('should export plugin', () => {
|
||||
expect({{ pluginVar }}).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
export const {{ pluginVar }} = createPlugin({
|
||||
id: '{{pluginId}}',
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
export const {{ extensionName }} = {{ pluginVar }}.provide(
|
||||
createRoutableExtension({
|
||||
name: '{{ extensionName }}',
|
||||
component: () =>
|
||||
import('./components/ExampleComponent').then(m => m.ExampleComponent),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: '{{pluginId}}',
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{packageName}}
|
||||
|
||||
The {{moduleId}} frontend module for the {{pluginId}} plugin.
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"description": "The {{moduleId}} frontend module for the {{pluginId}} plugin.",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin-module",
|
||||
"pluginId": "{{pluginId}}"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/frontend-plugin-api": "{{versionQuery '@backstage/frontend-plugin-api'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
name: frontend-plugin-module
|
||||
role: frontend-plugin-module
|
||||
description: A new frontend module that extends an existing frontend plugin
|
||||
values:
|
||||
moduleVar: '{{ camelCase pluginId }}Module{{ upperFirst ( camelCase moduleId ) }}'
|
||||
@@ -0,0 +1 @@
|
||||
export { {{ moduleVar }} as default } from './plugin';
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createFrontendModule } from '@backstage/frontend-plugin-api';
|
||||
|
||||
export const {{ moduleVar }} = createFrontendModule({
|
||||
pluginId: '{{ pluginId }}',
|
||||
extensions: [
|
||||
/* TODO */
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,20 @@
|
||||
# {{pluginId}}
|
||||
|
||||
Welcome to the {{pluginId}} plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
|
||||
## Getting started
|
||||
|
||||
Your plugin has been added to the app in this repository, meaning you'll be able
|
||||
to access it by running `yarn start` in the root directory, and then navigating
|
||||
to [/{{pluginId}}](http://localhost:3000/{{pluginId}}).
|
||||
|
||||
This plugin is built with Backstage's [new frontend
|
||||
system](https://backstage.io/docs/frontend-system/architecture/index), and you
|
||||
can find more information about building plugins in the [plugin builder
|
||||
documentation](https://backstage.io/docs/frontend-system/building-plugins/index).
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createDevApp } from '@backstage/frontend-dev-utils';
|
||||
|
||||
import plugin from '../src';
|
||||
|
||||
createDevApp({ features: [plugin] });
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin",
|
||||
"pluginId": "{{pluginId}}"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core-components": "{{versionQuery '@backstage/core-components'}}",
|
||||
"@backstage/frontend-plugin-api": "{{versionQuery '@backstage/frontend-plugin-api'}}",
|
||||
"@backstage/theme": "{{versionQuery '@backstage/theme'}}",
|
||||
"@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}",
|
||||
"@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}",
|
||||
"@material-ui/lab": "{{versionQuery '@material-ui/lab' '4.0.0-alpha.61'}}",
|
||||
"react-use": "{{versionQuery 'react-use' '17.2.4'}}"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@backstage/frontend-dev-utils": "{{versionQuery '@backstage/frontend-dev-utils'}}",
|
||||
"@backstage/frontend-test-utils": "{{versionQuery '@backstage/frontend-test-utils'}}",
|
||||
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}",
|
||||
"@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}",
|
||||
"@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}",
|
||||
"msw": "{{versionQuery 'msw' '1.0.0'}}",
|
||||
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
name: frontend-plugin
|
||||
role: frontend-plugin
|
||||
description: A new frontend plugin
|
||||
values:
|
||||
pluginVar: '{{ camelCase pluginId }}Plugin'
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { ExampleComponent } from './ExampleComponent';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { screen } from '@testing-library/react';
|
||||
import {
|
||||
registerMswTestHooks,
|
||||
renderInTestApp,
|
||||
} from '@backstage/frontend-test-utils';
|
||||
|
||||
describe('ExampleComponent', () => {
|
||||
const server = setupServer();
|
||||
// Enable sane handlers for network requests
|
||||
registerMswTestHooks(server);
|
||||
|
||||
// setup mock response
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render', async () => {
|
||||
await renderInTestApp(<ExampleComponent />);
|
||||
expect(
|
||||
screen.getByText('Welcome to {{pluginId}}!'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
import {
|
||||
InfoCard,
|
||||
Header,
|
||||
Page,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
import { ExampleFetchComponent } from '../ExampleFetchComponent';
|
||||
|
||||
export const ExampleComponent = () => (
|
||||
<Page themeId="tool">
|
||||
<Header title="Welcome to {{pluginId}}!" subtitle="Optional subtitle">
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Plugin title">
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard title="Information card">
|
||||
<Typography variant="body1">
|
||||
All content should be wrapped in a card like this.
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ExampleFetchComponent />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ExampleComponent } from './ExampleComponent';
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { renderInTestApp } from '@backstage/frontend-test-utils';
|
||||
import { ExampleFetchComponent } from './ExampleFetchComponent';
|
||||
|
||||
describe('ExampleFetchComponent', () => {
|
||||
it('renders the user table', async () => {
|
||||
const { getAllByText, getByAltText, getByText, findByRole } =
|
||||
await renderInTestApp(<ExampleFetchComponent />);
|
||||
|
||||
// Wait for the table to render
|
||||
const table = await findByRole('table');
|
||||
const nationality = getAllByText('GB');
|
||||
// Assert that the table contains the expected user data
|
||||
expect(table).toBeInTheDocument();
|
||||
expect(getByAltText('Carolyn')).toBeInTheDocument();
|
||||
expect(getByText('Carolyn Moore')).toBeInTheDocument();
|
||||
expect(getByText('carolyn.moore@example.com')).toBeInTheDocument();
|
||||
expect(nationality[0]).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
Table,
|
||||
TableColumn,
|
||||
Progress,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
export const exampleUsers = {
|
||||
results: [
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Miss',
|
||||
first: 'Carolyn',
|
||||
last: 'Moore',
|
||||
},
|
||||
email: 'carolyn.moore@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Carolyn',
|
||||
nat: 'GB',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Esma',
|
||||
last: 'Berberoğlu',
|
||||
},
|
||||
email: 'esma.berberoglu@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Esma',
|
||||
nat: 'TR',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Isabella',
|
||||
last: 'Rhodes',
|
||||
},
|
||||
email: 'isabella.rhodes@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella',
|
||||
nat: 'GB',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Derrick',
|
||||
last: 'Carter',
|
||||
},
|
||||
email: 'derrick.carter@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Derrick',
|
||||
nat: 'IE',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Miss',
|
||||
first: 'Mattie',
|
||||
last: 'Lambert',
|
||||
},
|
||||
email: 'mattie.lambert@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mattie',
|
||||
nat: 'AU',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Mijat',
|
||||
last: 'Rakić',
|
||||
},
|
||||
email: 'mijat.rakic@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mijat',
|
||||
nat: 'RS',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Javier',
|
||||
last: 'Reid',
|
||||
},
|
||||
email: 'javier.reid@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Javier',
|
||||
nat: 'US',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Isabella',
|
||||
last: 'Li',
|
||||
},
|
||||
email: 'isabella.li@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella',
|
||||
nat: 'CA',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Mrs',
|
||||
first: 'Stephanie',
|
||||
last: 'Garrett',
|
||||
},
|
||||
email: 'stephanie.garrett@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Stephanie',
|
||||
nat: 'AU',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Antonia',
|
||||
last: 'Núñez',
|
||||
},
|
||||
email: 'antonia.nunez@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Antonia',
|
||||
nat: 'ES',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Donald',
|
||||
last: 'Young',
|
||||
},
|
||||
email: 'donald.young@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Donald',
|
||||
nat: 'US',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Iegor',
|
||||
last: 'Holodovskiy',
|
||||
},
|
||||
email: 'iegor.holodovskiy@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Iegor',
|
||||
nat: 'UA',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Madame',
|
||||
first: 'Jessica',
|
||||
last: 'David',
|
||||
},
|
||||
email: 'jessica.david@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jessica',
|
||||
nat: 'CH',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Eve',
|
||||
last: 'Martinez',
|
||||
},
|
||||
email: 'eve.martinez@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Eve',
|
||||
nat: 'FR',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Caleb',
|
||||
last: 'Silva',
|
||||
},
|
||||
email: 'caleb.silva@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Caleb',
|
||||
nat: 'US',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Miss',
|
||||
first: 'Marcia',
|
||||
last: 'Jenkins',
|
||||
},
|
||||
email: 'marcia.jenkins@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Marcia',
|
||||
nat: 'US',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Mrs',
|
||||
first: 'Mackenzie',
|
||||
last: 'Jones',
|
||||
},
|
||||
email: 'mackenzie.jones@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mackenzie',
|
||||
nat: 'NZ',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Jeremiah',
|
||||
last: 'Gutierrez',
|
||||
},
|
||||
email: 'jeremiah.gutierrez@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jeremiah',
|
||||
nat: 'AU',
|
||||
},
|
||||
{
|
||||
gender: 'female',
|
||||
name: {
|
||||
title: 'Ms',
|
||||
first: 'Luciara',
|
||||
last: 'Souza',
|
||||
},
|
||||
email: 'luciara.souza@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Luciara',
|
||||
nat: 'BR',
|
||||
},
|
||||
{
|
||||
gender: 'male',
|
||||
name: {
|
||||
title: 'Mr',
|
||||
first: 'Valgi',
|
||||
last: 'da Cunha',
|
||||
},
|
||||
email: 'valgi.dacunha@example.com',
|
||||
picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Valgi',
|
||||
nat: 'BR',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
avatar: {
|
||||
height: 32,
|
||||
width: 32,
|
||||
borderRadius: '50%',
|
||||
},
|
||||
});
|
||||
|
||||
type User = {
|
||||
gender: string; // "male"
|
||||
name: {
|
||||
title: string; // "Mr",
|
||||
first: string; // "Duane",
|
||||
last: string; // "Reed"
|
||||
};
|
||||
email: string; // "duane.reed@example.com"
|
||||
picture: string; // "https://api.dicebear.com/6.x/open-peeps/svg?seed=Duane"
|
||||
nat: string; // "AU"
|
||||
};
|
||||
|
||||
type DenseTableProps = {
|
||||
users: User[];
|
||||
};
|
||||
|
||||
export const DenseTable = ({ users }: DenseTableProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ title: 'Avatar', field: 'avatar' },
|
||||
{ title: 'Name', field: 'name' },
|
||||
{ title: 'Email', field: 'email' },
|
||||
{ title: 'Nationality', field: 'nationality' },
|
||||
];
|
||||
|
||||
const data = users.map(user => {
|
||||
return {
|
||||
avatar: (
|
||||
<img
|
||||
src={user.picture}
|
||||
className={classes.avatar}
|
||||
alt={user.name.first}
|
||||
/>
|
||||
),
|
||||
name: `${user.name.first} ${user.name.last}`,
|
||||
email: user.email,
|
||||
nationality: user.nat,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="Example User List"
|
||||
options=\{{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExampleFetchComponent = () => {
|
||||
|
||||
const { value, loading, error } = useAsync(async (): Promise<User[]> => {
|
||||
// Would use fetch in a real world example
|
||||
return exampleUsers.results;
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <ResponseErrorPanel error={error} />;
|
||||
}
|
||||
|
||||
return <DenseTable users={value || []} />;
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ExampleFetchComponent } from './ExampleFetchComponent';
|
||||
@@ -0,0 +1 @@
|
||||
export { {{ pluginVar }} as default } from './plugin';
|
||||
@@ -0,0 +1,7 @@
|
||||
import { {{ pluginVar }} } from './plugin';
|
||||
|
||||
describe('{{pluginId}}', () => {
|
||||
it('should export plugin', () => {
|
||||
expect({{ pluginVar }}).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
createFrontendPlugin,
|
||||
PageBlueprint,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
export const page = PageBlueprint.make({
|
||||
params: {
|
||||
path: '/{{pluginId}}',
|
||||
routeRef: rootRouteRef,
|
||||
loader: () =>
|
||||
import('./components/ExampleComponent').then(m =>
|
||||
<m.ExampleComponent />,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
export const {{ pluginVar }} = createFrontendPlugin({
|
||||
pluginId: '{{pluginId}}',
|
||||
extensions: [page],
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createRouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef();
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,12 @@
|
||||
# {{packageName}}
|
||||
|
||||
_This package was created through the Backstage CLI_.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the package via Yarn:
|
||||
|
||||
```sh
|
||||
cd <package-dir> # if within a monorepo
|
||||
yarn add {{packageName}}
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}",
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
name: node-library
|
||||
role: node-library
|
||||
description: A library package, exporting shared functionality for Node.js environments
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{packageName}}
|
||||
|
||||
Welcome to the common package for the {{pluginId}} plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"description": "Common functionalities for the {{pluginId}} plugin",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "common-library",
|
||||
"pluginId": "{{pluginId}}"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
name: plugin-common-library
|
||||
role: plugin-common-library
|
||||
description: A new isomorphic common plugin package
|
||||
@@ -0,0 +1,19 @@
|
||||
/***/
|
||||
/**
|
||||
* Common functionalities for the {{pluginId}} plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
/**
|
||||
* In this package you might for example declare types that are common
|
||||
* between the frontend and backend plugin packages.
|
||||
*/
|
||||
export type CommonType = {
|
||||
field: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Or you might declare some common constants.
|
||||
*/
|
||||
export const COMMON_CONSTANT = 1;
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{packageName}}
|
||||
|
||||
Welcome to the Node.js library package for the {{pluginId}} plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"description": "Node.js library for the {{pluginId}} plugin",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "node-library",
|
||||
"pluginId": "{{pluginId}}"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}",
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
name: plugin-node-library
|
||||
role: plugin-node-library
|
||||
description: A new Node.js library plugin package
|
||||
@@ -0,0 +1,18 @@
|
||||
/***/
|
||||
/**
|
||||
* Node.js library for the {{pluginId}} plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// In this package you might for example export functions that
|
||||
// help other plugins or modules interact with your plugin.
|
||||
|
||||
/**
|
||||
* Does something useful.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function someFunction() {
|
||||
// ...
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# {{packageName}}
|
||||
|
||||
Welcome to the web library package for the {{pluginId}} plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "{{packageName}}",
|
||||
"description": "Web library for the {{pluginId}} plugin",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "web-library",
|
||||
"pluginId": "{{pluginId}}"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core-plugin-api": "{{versionQuery '@backstage/core-plugin-api'}}",
|
||||
"@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}",
|
||||
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}",
|
||||
"@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}",
|
||||
"react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
name: plugin-web-library
|
||||
role: plugin-web-library
|
||||
description: A new web library plugin package
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user