Merge pull request #1724 from spotify/feat/backend-plugin

Create backend plugin through CLI
This commit is contained in:
Ben Lambert
2020-09-24 14:18:06 +02:00
committed by GitHub
40 changed files with 388 additions and 70 deletions
@@ -231,7 +231,11 @@ export default async (cmd: Command) => {
const privatePackage = cmd.private === false ? false : true;
const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json'));
const appPackage = paths.resolveTargetRoot('packages/app');
const templateDir = paths.resolveOwn('templates/default-plugin');
const templateDir = paths.resolveOwn(
cmd.backend
? 'templates/default-backend-plugin'
: 'templates/default-plugin',
);
const tempDir = resolvePath(os.tmpdir(), answers.id);
const pluginDir = isMonoRepo
? paths.resolveTargetRoot('plugins', answers.id)
@@ -252,6 +256,7 @@ export default async (cmd: Command) => {
await createTemporaryPluginFolder(tempDir);
Task.section('Preparing files');
await templatingTask(templateDir, tempDir, {
...answers,
version,
@@ -267,7 +272,7 @@ export default async (cmd: Command) => {
Task.section('Building the plugin');
await buildPlugin(pluginDir);
if (await fs.pathExists(appPackage)) {
if ((await fs.pathExists(appPackage)) && !cmd.backend) {
Task.section('Adding plugin as dependency in app');
await addPluginDependencyToApp(paths.targetRoot, name, version);
+4
View File
@@ -61,6 +61,10 @@ export function registerCommands(program: CommanderStatic) {
program
.command('create-plugin')
.option(
'--backend',
'Create plugin with the backend dependencies as default',
)
.description('Creates a new plugin in the current repository')
.option('--scope <scope>', 'NPM scope')
.option('--npm-registry <URL>', 'NPM registry URL')
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
@@ -0,0 +1,14 @@
# {{id}}
Welcome to the {{id}} backend 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 [/{{id}}](http://localhost:3000/{{id}}).
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,44 @@
{
"name": "{{name}}",
"version": "{{version}}",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
{{#if privatePackage}} "private": {{privatePackage}},
{{/if}}
"publishConfig": {
{{#if npmRegistry}} "registry": "{{npmRegistry}}",
{{/if}}
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"scripts": {
"start": "backstage-cli backend:dev",
"build": "backstage-cli backend:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^{{version}}",
"@backstage/config": "^{{version}}",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"winston": "^3.2.1",
"node-fetch": "^2.6.1",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^{{version}}",
"@types/supertest": "^2.0.8",
"supertest": "^4.0.2",
"msw": "^0.20.5"
},
"files": [
"dist"
]
}
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
export * from './service/router';
@@ -0,0 +1,33 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { getRootLogger } from '@backstage/backend-common';
import yn from 'yn';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch(err => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
@@ -0,0 +1,45 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
describe('createRouter', () => {
let app: express.Express;
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
});
app = express().use(router);
});
beforeEach(() => {
jest.resetAllMocks();
});
describe('GET /health', () => {
it('returns ok', async () => {
const response = await request(app).get('/health');
expect(response.status).toEqual(200);
expect(response.body).toEqual({ status: 'ok' });
});
});
});
@@ -0,0 +1,40 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { errorHandler } from '@backstage/backend-common';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
export interface RouterOptions {
logger: Logger;
}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger } = options;
const router = Router();
router.use(express.json());
router.get('/health', (_, response) => {
logger.info('PONG!');
response.send({ status: 'ok' });
});
router.use(errorHandler());
return router;
}
@@ -0,0 +1,47 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { createServiceBuilder } from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: '{{id}}-backend' });
logger.debug('Starting application server...');
const router = await createRouter({
logger,
});
const service = createServiceBuilder(module)
.enableCors({ origin: 'http://localhost:3000' })
.addRouter('/{{id}}', router);
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
@@ -0,0 +1,18 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
export {};
global.fetch = require('node-fetch');
@@ -41,7 +41,8 @@
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"jest-fetch-mock": "^3.0.3"
"msw": "^0.20.5",
"node-fetch": "^2.6.1"
},
"files": [
"dist"
@@ -1,18 +1,35 @@
import React from 'react';
import { render } from '@testing-library/react';
import mockFetch from 'jest-fetch-mock';
import ExampleComponent from './ExampleComponent';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
describe('ExampleComponent', () => {
const server = setupServer();
// Enable API mocking before tests.
beforeAll(() => server.listen())
// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())
// Disable API mocking after the tests are done.
afterAll(() => server.close())
// setup mock response
beforeEach(() => {
server.use(rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))))
})
it('should render', () => {
mockFetch.mockResponse(() => new Promise(() => {}));
const rendered = render(
<ThemeProvider theme={lightTheme}>
<ExampleComponent />
</ThemeProvider>,
);
expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
);
expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
});
});
@@ -1,11 +1,25 @@
import React from 'react';
import { render } from '@testing-library/react';
import mockFetch from 'jest-fetch-mock';
import ExampleFetchComponent from './ExampleFetchComponent';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
describe('ExampleFetchComponent', () => {
const server = setupServer();
// Enable API mocking before tests.
beforeAll(() => server.listen())
// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())
// Disable API mocking after the tests are done.
afterAll(() => server.close())
// setup mock response
beforeEach(() => {
server.use(rest.get('https://randomuser.me/*', (_, res, ctx) => res(ctx.status(200), ctx.delay(2000), ctx.json({}))))
})
it('should render', async () => {
mockFetch.mockResponse(() => new Promise(() => {}));
const rendered = render(<ExampleFetchComponent />);
expect(await rendered.findByTestId('progress')).toBeInTheDocument();
});
@@ -1,3 +1,2 @@
import '@testing-library/jest-dom';
require('jest-fetch-mock').enableMocks();
global.fetch = require('node-fetch');
+9 -2
View File
@@ -55,6 +55,9 @@ async function main() {
print('Creating a Backstage Plugin');
const pluginName = await createPlugin('test-plugin', appDir);
print('Creating a Backstage Backend Plugin');
await createPlugin('test-backend-plugin', appDir, ['--backend']);
print('Starting the app');
await testAppServe(pluginName, appDir);
@@ -238,8 +241,12 @@ async function overrideModuleResolutions(appDir: string, workspaceDir: string) {
/**
* Uses create-plugin command to create a new plugin in the app
*/
async function createPlugin(pluginName: string, appDir: string) {
const child = spawnPiped(['yarn', 'create-plugin'], {
async function createPlugin(
pluginName: string,
appDir: string,
options: string[] = [],
) {
const child = spawnPiped(['yarn', 'create-plugin', ...options], {
cwd: appDir,
});