Merge pull request #4251 from backstage/rugvip/extmpl

cli: update frontend plugin template to use new extension API
This commit is contained in:
Patrik Oldsberg
2021-02-01 13:01:47 +01:00
committed by GitHub
21 changed files with 188 additions and 123 deletions
@@ -20,6 +20,8 @@ import chalk from 'chalk';
import inquirer, { Answers, Question } from 'inquirer';
import { exec as execCb } from 'child_process';
import { resolve as resolvePath, join as joinPath } from 'path';
import camelCase from 'lodash/camelCase';
import upperFirst from 'lodash/upperFirst';
import os from 'os';
import { Command } from 'commander';
import {
@@ -104,16 +106,12 @@ export async function addPluginDependencyToApp(
});
}
export async function addPluginToApp(
export async function addPluginImportToApp(
rootDir: string,
pluginName: string,
pluginVar: string,
pluginPackage: string,
) {
const pluginNameCapitalized = pluginName
.split('-')
.map(name => capitalize(name))
.join('');
const pluginExport = `export { plugin as ${pluginNameCapitalized} } from '${pluginPackage}';`;
const pluginExport = `export { ${pluginVar} } from '${pluginPackage}';`;
const pluginsFilePath = 'packages/app/src/plugins.ts';
const pluginsFile = resolvePath(rootDir, pluginsFilePath);
@@ -126,6 +124,46 @@ export async function addPluginToApp(
});
}
export async function addPluginExtensionToApp(
pluginId: string,
extensionName: string,
pluginPackage: string,
) {
const pluginsFilePath = paths.resolveTargetRoot('packages/app/src/App.tsx');
if (!(await fs.pathExists(pluginsFilePath))) {
return;
}
await Task.forItem('processing', pluginsFilePath, async () => {
const content = await fs.readFile(pluginsFilePath, 'utf8');
const revLines = content.split('\n').reverse();
const lastImportIndex = revLines.findIndex(line =>
line.match(/ from ("|').*("|')/),
);
const lastRouteIndex = revLines.findIndex(line =>
line.match(/<\/FlatRoutes/),
);
if (lastImportIndex !== -1 && lastRouteIndex !== -1) {
revLines.splice(
lastImportIndex,
0,
`import { ${extensionName} } from '${pluginPackage}';`,
);
const [indentation] = revLines[lastRouteIndex + 1].match(/^\s*/) ?? [];
revLines.splice(
lastRouteIndex + 1,
0,
`${indentation}<Route path="/${pluginId}" element={<${extensionName} />}/>`,
);
const newContent = revLines.reverse().join('\n');
await fs.writeFile(pluginsFilePath, newContent, 'utf8');
}
});
}
async function cleanUp(tempDir: string) {
await Task.forItem('remove', 'temporary directory', async () => {
await fs.remove(tempDir);
@@ -223,6 +261,8 @@ export default async (cmd: Command) => {
const name = cmd.scope
? `@${cmd.scope.replace(/^@/, '')}/plugin-${pluginId}`
: `plugin-${pluginId}`;
const pluginVar = `${camelCase(answers.id)}Plugin`;
const extensionName = `${upperFirst(camelCase(answers.id))}Page`;
const npmRegistry = cmd.npmRegistry && cmd.scope ? cmd.npmRegistry : '';
const privatePackage = cmd.private === false ? false : true;
const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json'));
@@ -259,7 +299,9 @@ export default async (cmd: Command) => {
tempDir,
{
...answers,
pluginVar,
pluginVersion,
extensionName,
name,
privatePackage,
npmRegistry,
@@ -278,7 +320,8 @@ export default async (cmd: Command) => {
await addPluginDependencyToApp(paths.targetRoot, name, pluginVersion);
Task.section('Import plugin in app');
await addPluginToApp(paths.targetRoot, pluginId, name);
await addPluginImportToApp(paths.targetRoot, pluginVar, name);
await addPluginExtensionToApp(pluginId, extensionName, name);
}
if (ownerIds && ownerIds.length) {
+5
View File
@@ -39,6 +39,11 @@ const fileHandlers = [
patterns: ['package.json'],
handler: handlers.packageJson,
},
{
// Not all plugins have routes
patterns: ['src/routes.ts'],
handler: handlers.skip,
},
{
// make sure files in 1st level of src/ and dev/ exist
patterns: ['.eslintrc.js', /^(src|dev)\/[^/]+$/],
@@ -1,4 +0,0 @@
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
@@ -0,0 +1,11 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { {{ pluginVar }}, {{ extensionName }} } from '../src/plugin';
createDevApp()
.registerPlugin({{ pluginVar }})
.addPage({
element: <{{ extensionName }} />,
title: 'Root Page',
})
.render();
@@ -1,13 +1,12 @@
import React from 'react';
import { render } from '@testing-library/react';
import ExampleComponent from './ExampleComponent';
import { ExampleComponent } from './ExampleComponent';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { msw } from '@backstage/test-utils';
describe('ExampleComponent', () => {
const server = setupServer();
// Enable sane handlers for network requests
@@ -15,15 +14,17 @@ describe('ExampleComponent', () => {
// setup mock response
beforeEach(() => {
server.use(rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))))
})
server.use(
rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))),
);
});
it('should render', () => {
const rendered = render(
<ThemeProvider theme={lightTheme}>
<ExampleComponent />
</ThemeProvider>,
);
expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
);
expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
});
});
@@ -9,9 +9,9 @@ import {
HeaderLabel,
SupportButton,
} from '@backstage/core';
import ExampleFetchComponent from '../ExampleFetchComponent';
import { ExampleFetchComponent } from '../ExampleFetchComponent';
const ExampleComponent = () => (
export const ExampleComponent = () => (
<Page themeId="tool">
<Header title="Welcome to {{ id }}!" subtitle="Optional subtitle">
<HeaderLabel label="Owner" value="Team X" />
@@ -36,5 +36,3 @@ const ExampleComponent = () => (
</Content>
</Page>
);
export default ExampleComponent;
@@ -1 +1 @@
export { default } from './ExampleComponent';
export { ExampleComponent } from './ExampleComponent';
@@ -1,6 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react';
import ExampleFetchComponent from './ExampleFetchComponent';
import { ExampleFetchComponent } from './ExampleFetchComponent';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { msw } from '@backstage/test-utils';
@@ -9,11 +9,15 @@ describe('ExampleFetchComponent', () => {
const server = setupServer();
// Enable sane handlers for network requests
msw.setupDefaultHandlers(server);
// setup mock response
beforeEach(() => {
server.use(rest.get('https://randomuser.me/*', (_, res, ctx) => res(ctx.status(200), ctx.delay(2000), ctx.json({}))))
})
server.use(
rest.get('https://randomuser.me/*', (_, res, ctx) =>
res(ctx.status(200), ctx.delay(2000), ctx.json({})),
),
);
});
it('should render', async () => {
const rendered = render(<ExampleFetchComponent />);
expect(await rendered.findByTestId('progress')).toBeInTheDocument();
@@ -48,7 +48,7 @@ export const DenseTable = ({ users }: DenseTableProps) => {
{ title: 'Nationality', field: 'nationality' },
];
const data = users.map((user) => {
const data = users.map(user => {
return {
avatar: (
<img
@@ -73,7 +73,7 @@ export const DenseTable = ({ users }: DenseTableProps) => {
);
};
const ExampleFetchComponent = () => {
export const ExampleFetchComponent = () => {
const { value, loading, error } = useAsync(async (): Promise<User[]> => {
const response = await fetch('https://randomuser.me/api/?results=20');
const data = await response.json();
@@ -88,5 +88,3 @@ const ExampleFetchComponent = () => {
return <DenseTable users={value || []} />;
};
export default ExampleFetchComponent;
@@ -1 +1 @@
export { default } from './ExampleFetchComponent';
export { ExampleFetchComponent } from './ExampleFetchComponent';
@@ -1 +0,0 @@
export { plugin } from './plugin';
@@ -0,0 +1 @@
export { {{ pluginVar }}, {{ extensionName }} } from './plugin';
@@ -1,7 +1,7 @@
import { plugin } from './plugin';
import { {{ pluginVar }} } from './plugin';
describe('{{ id }}', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
expect({{ pluginVar }}).toBeDefined();
});
});
@@ -1,14 +1,18 @@
import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
import { createPlugin, createRoutableExtension } from '@backstage/core';
export const rootRouteRef = createRouteRef({
path: '/{{ id }}',
title: '{{ id }}',
});
import { rootRouteRef } from './routes';
export const plugin = createPlugin({
export const {{ pluginVar }} = createPlugin({
id: '{{ id }}',
register({ router }) {
router.addRoute(rootRouteRef, ExampleComponent);
routes: {
root: rootRouteRef,
},
});
export const {{ extensionName }} = {{ pluginVar }}.provide(
createRoutableExtension({
component: () =>
import('./components/ExampleComponent').then(m => m.ExampleComponent),
mountPoint: rootRouteRef,
}),
);
@@ -0,0 +1,5 @@
import { createRouteRef } from '@backstage/core';
export const rootRouteRef = createRouteRef({
title: '{{ id }}',
});