From a7d8a03e31c2bf8569077861fb7d644053cf1af2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 26 Apr 2020 13:37:53 +0200 Subject: [PATCH] packages: add new dev-utils package --- packages/dev-utils/.eslintrc.js | 3 + packages/dev-utils/.npmrc | 1 + packages/dev-utils/README.md | 24 ++++ packages/dev-utils/package.json | 47 +++++++ packages/dev-utils/src/devApp/apiFactories.ts | 47 +++++++ packages/dev-utils/src/devApp/index.tsx | 17 +++ packages/dev-utils/src/devApp/render.tsx | 124 ++++++++++++++++++ packages/dev-utils/src/index.ts | 16 +++ packages/dev-utils/src/setupTests.ts | 17 +++ packages/dev-utils/tsconfig.json | 7 + 10 files changed, 303 insertions(+) create mode 100644 packages/dev-utils/.eslintrc.js create mode 100644 packages/dev-utils/.npmrc create mode 100644 packages/dev-utils/README.md create mode 100644 packages/dev-utils/package.json create mode 100644 packages/dev-utils/src/devApp/apiFactories.ts create mode 100644 packages/dev-utils/src/devApp/index.tsx create mode 100644 packages/dev-utils/src/devApp/render.tsx create mode 100644 packages/dev-utils/src/index.ts create mode 100644 packages/dev-utils/src/setupTests.ts create mode 100644 packages/dev-utils/tsconfig.json diff --git a/packages/dev-utils/.eslintrc.js b/packages/dev-utils/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/dev-utils/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/dev-utils/.npmrc b/packages/dev-utils/.npmrc new file mode 100644 index 0000000000..214c29d139 --- /dev/null +++ b/packages/dev-utils/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/packages/dev-utils/README.md b/packages/dev-utils/README.md new file mode 100644 index 0000000000..d082b7d06b --- /dev/null +++ b/packages/dev-utils/README.md @@ -0,0 +1,24 @@ +# @backstage/dev-utils + +Utilities for developing Backstage plugins. + +This package provides utilities that help in developing plugins for backstage, like App wrappers and API implementations for standalone serving of apps. + +## Installation + +Install the package via npm or yarn: + +```sh +$ npm install --save-dev @backstage/dev-utils +``` + +or + +```sh +$ yarn add -D @backstage/dev-utils +``` + +## Documentation + +- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md) diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json new file mode 100644 index 0000000000..2a241f6a9a --- /dev/null +++ b/packages/dev-utils/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/dev-utils", + "description": "Utilities for developing Backstage plugins.", + "version": "0.1.1-alpha.4", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/spotify/backstage", + "directory": "packages/dev-utils" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/cli": "^0.1.1-alpha.4", + "@backstage/core": "^0.1.1-alpha.4", + "@backstage/test-utils": "^0.1.1-alpha.4", + "@backstage/theme": "^0.1.1-alpha.4", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^7.1.2", + "@types/jest": "^24.0.0", + "@types/node": "^12.0.0", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "react-router": "^5.1.2", + "react-router-dom": "^5.1.2" + }, + "files": [ + "dist" + ] +} diff --git a/packages/dev-utils/src/devApp/apiFactories.ts b/packages/dev-utils/src/devApp/apiFactories.ts new file mode 100644 index 0000000000..a03faf0033 --- /dev/null +++ b/packages/dev-utils/src/devApp/apiFactories.ts @@ -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 { + alertApiRef, + errorApiRef, + ErrorApiForwarder, + AlertApi, + createApiFactory, +} from '@backstage/core'; + +// TODO(rugvip): We should likely figure out how to reuse all of these between apps +// and plugin serve with minimal boilerplate. For example we might move everything +// to DI, and provide factories for the default implementations, so this just becomes +// a list of things like `[ErrorApiForwarder.factory, AlertApiDialog.factory]`. + +export const alertApiFactory = createApiFactory({ + implements: alertApiRef, + deps: {}, + factory: (): AlertApi => ({ + // TODO: Figure out how to ship a nicer implementation without having + // to export any external references. + post(alertConfig) { + // eslint-disable-next-line no-alert + alert(`Alert[${alertConfig.severity}]: ${alertConfig.message}`); + }, + }), +}); + +export const errorApiFactory = createApiFactory({ + implements: errorApiRef, + deps: { alertApi: alertApiRef }, + factory: ({ alertApi }) => new ErrorApiForwarder(alertApi), +}); diff --git a/packages/dev-utils/src/devApp/index.tsx b/packages/dev-utils/src/devApp/index.tsx new file mode 100644 index 0000000000..b832b0d2f6 --- /dev/null +++ b/packages/dev-utils/src/devApp/index.tsx @@ -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 './render'; diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx new file mode 100644 index 0000000000..1919afd8e0 --- /dev/null +++ b/packages/dev-utils/src/devApp/render.tsx @@ -0,0 +1,124 @@ +/* + * 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 React, { FC } from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter } from 'react-router-dom'; +import BookmarkIcon from '@material-ui/icons/Bookmark'; +import { ThemeProvider, CssBaseline } from '@material-ui/core'; +import { + createApp, + SidebarPage, + Sidebar, + SidebarItem, + SidebarSpacer, + ApiFactory, + createPlugin, + ApiTestRegistry, + ApiHolder, +} from '@backstage/core'; +import { lightTheme } from '@backstage/theme'; +import * as apiFactories from './apiFactories'; + +type BackstagePlugin = ReturnType; + +export type RenderInAppConfig = { + plugins: BackstagePlugin[]; + apis: ApiFactory[]; +}; + +// Set up an API registry that merges together default implementations with ones provided through config. +function setupApiRegistry(config: RenderInAppConfig): ApiHolder { + const providedApis = new Set(config.apis.map(factory => factory.implements)); + + // Exlude any default API factory that we receive a factory for in the config + const defaultFactories = Object.values(apiFactories).filter(factory => + providedApis.has(factory.implements), + ); + const allFactories = [...defaultFactories, ...config.apis]; + + // Use a test registry with dependency injection so that the consumer + // can override APIs but still depend on the default implementations. + const registry = new ApiTestRegistry(); + for (const factory of allFactories) { + registry.register(factory); + } + + return registry; +} + +// Create a sidebar that exposes the touchpoints of a plugin +function setupSidebar(config: RenderInAppConfig): JSX.Element { + const sidebarItems = new Array(); + + for (const plugin of config.plugins) { + for (const output of plugin.output()) { + switch (output.type) { + case 'route': { + const { path } = output; + sidebarItems.push( + , + ); + break; + } + default: + break; + } + } + } + + return ( + + + {sidebarItems} + + ); +} + +// TODO(rugvip): Figure out patterns for how to allow in-house apps to build upon +// this to provide their own plugin dev wrappers. + +// Renders one or more plugins in a dev app and exposes outputs from the plugin +export function renderPluginsInApp(config: RenderInAppConfig): void { + const app = createApp(); + app.registerApis(setupApiRegistry(config)); + app.registerPlugin(...config.plugins); + const AppComponent = app.build(); + + const sidebar = setupSidebar(config); + + const App: FC<{}> = () => { + return ( + + + + + {sidebar} + + + + + + ); + }; + + ReactDOM.render(, document.getElementById('root')); +} diff --git a/packages/dev-utils/src/index.ts b/packages/dev-utils/src/index.ts new file mode 100644 index 0000000000..c05e67ddbc --- /dev/null +++ b/packages/dev-utils/src/index.ts @@ -0,0 +1,16 @@ +/* + * 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 './devApp'; diff --git a/packages/dev-utils/src/setupTests.ts b/packages/dev-utils/src/setupTests.ts new file mode 100644 index 0000000000..8925258421 --- /dev/null +++ b/packages/dev-utils/src/setupTests.ts @@ -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. + */ + +import '@testing-library/jest-dom/extend-expect'; diff --git a/packages/dev-utils/tsconfig.json b/packages/dev-utils/tsconfig.json new file mode 100644 index 0000000000..7b73db2f0f --- /dev/null +++ b/packages/dev-utils/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "baseUrl": "src" + } +}