From a1831f8c53e3d64a661101b7365e83df1de18fa3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 26 Apr 2020 13:16:50 +0200 Subject: [PATCH 1/9] packages/core: add helper for inferring ApiFactory types --- packages/core/src/api/apis/helpers.ts | 26 ++++++++++++++++++++++++++ packages/core/src/api/apis/index.ts | 1 + packages/core/src/api/apis/types.ts | 8 ++++---- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/api/apis/helpers.ts diff --git a/packages/core/src/api/apis/helpers.ts b/packages/core/src/api/apis/helpers.ts new file mode 100644 index 0000000000..1a46781c58 --- /dev/null +++ b/packages/core/src/api/apis/helpers.ts @@ -0,0 +1,26 @@ +/* + * 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 { ApiFactory } from './types'; + +// Used to infer types for a standalone ApiFactory that isn't immediately passed +// to another function. +// This function doesn't actually do anything, it's only used to infer types. +export function createApiFactory( + factory: ApiFactory, +): ApiFactory { + return factory; +} diff --git a/packages/core/src/api/apis/index.ts b/packages/core/src/api/apis/index.ts index 8dd982264d..9ab9a25e50 100644 --- a/packages/core/src/api/apis/index.ts +++ b/packages/core/src/api/apis/index.ts @@ -19,5 +19,6 @@ export { default as ApiRegistry } from './ApiRegistry'; export { default as ApiTestRegistry } from './ApiTestRegistry'; export { default as ApiRef } from './ApiRef'; export * from './types'; +export * from './helpers'; export * from './definitions'; export * from './implementations'; diff --git a/packages/core/src/api/apis/types.ts b/packages/core/src/api/apis/types.ts index 129afdaaa5..c44fd804a5 100644 --- a/packages/core/src/api/apis/types.ts +++ b/packages/core/src/api/apis/types.ts @@ -30,8 +30,8 @@ export type ApiHolder = { get(api: ApiRef): T | undefined; }; -export type ApiFactory = { - implements: ApiRef; - deps: TypesToApiRefs; - factory(deps: D): I extends A ? I : never; +export type ApiFactory = { + implements: ApiRef; + deps: TypesToApiRefs; + factory(deps: Deps): Impl extends Api ? Impl : never; }; From a7d8a03e31c2bf8569077861fb7d644053cf1af2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 26 Apr 2020 13:37:53 +0200 Subject: [PATCH 2/9] 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" + } +} From 53c0d19e8de3ec46932d2f5ceb84215b7bf88e52 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 26 Apr 2020 13:38:26 +0200 Subject: [PATCH 3/9] plugins/graphiql: use dev-utils to set up dev serve --- plugins/graphiql/dev/index.tsx | 76 +++++++++++----------------------- plugins/graphiql/package.json | 1 + 2 files changed, 26 insertions(+), 51 deletions(-) diff --git a/plugins/graphiql/dev/index.tsx b/plugins/graphiql/dev/index.tsx index ed987c1c6a..0dc48f1924 100644 --- a/plugins/graphiql/dev/index.tsx +++ b/plugins/graphiql/dev/index.tsx @@ -14,56 +14,30 @@ * limitations under the License. */ -import React, { FC } from 'react'; -import ReactDOM from 'react-dom'; -import { BrowserRouter } from 'react-router-dom'; -import HomeIcon from '@material-ui/icons/Home'; -import { ThemeProvider, CssBaseline } from '@material-ui/core'; -import { - createApp, - SidebarPage, - Sidebar, - SidebarItem, - SidebarSpacer, - ApiRegistry, -} from '@backstage/core'; -import { lightTheme } from '@backstage/theme'; +import { renderPluginsInApp } from '@backstage/dev-utils'; import { plugin, GraphQLEndpoints, graphQlBrowseApiRef } from '../src'; +import { createApiFactory } from '@backstage/core'; -const graphQlBrowseApi = GraphQLEndpoints.from([ - GraphQLEndpoints.create({ - id: 'gitlab', - title: 'GitLab', - url: 'https://gitlab.com/api/graphql', - }), - GraphQLEndpoints.create({ - id: 'countries', - title: 'Countries', - url: 'https://countries.trevorblades.com/', - }), -]); - -const app = createApp(); -app.registerApis(ApiRegistry.from([[graphQlBrowseApiRef, graphQlBrowseApi]])); -app.registerPlugin(plugin); -const AppComponent = app.build(); - -const App: FC<{}> = () => { - return ( - - - - - - - - - - - - - - ); -}; - -ReactDOM.render(, document.getElementById('root')); +renderPluginsInApp({ + plugins: [plugin], + apis: [ + createApiFactory({ + implements: graphQlBrowseApiRef, + deps: {}, + factory() { + return GraphQLEndpoints.from([ + GraphQLEndpoints.create({ + id: 'gitlab', + title: 'GitLab', + url: 'https://gitlab.com/api/graphql', + }), + GraphQLEndpoints.create({ + id: 'countries', + title: 'Countries', + url: 'https://countries.trevorblades.com/', + }), + ]); + }, + }), + ], +}); diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index f2cf8b7930..d52957cf84 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@backstage/core": "^0.1.1-alpha.4", + "@backstage/dev-utils": "^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", From 84877f6baea52d7f74f6d55130737f52dfa49c1d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 26 Apr 2020 13:48:26 +0200 Subject: [PATCH 4/9] packages/dev-utils: add test for api factories --- .../dev-utils/src/devApp/apiFactories.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 packages/dev-utils/src/devApp/apiFactories.test.ts diff --git a/packages/dev-utils/src/devApp/apiFactories.test.ts b/packages/dev-utils/src/devApp/apiFactories.test.ts new file mode 100644 index 0000000000..55455346b9 --- /dev/null +++ b/packages/dev-utils/src/devApp/apiFactories.test.ts @@ -0,0 +1,34 @@ +/* + * 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 * as apiFactories from './apiFactories'; +import { ApiTestRegistry } from '@backstage/core'; + +describe('apiFactories', () => { + it('should be possible to get an instance of each API', () => { + const registry = new ApiTestRegistry(); + const factories = Object.values(apiFactories); + + for (const factory of factories) { + registry.register(factory); + } + + for (const factory of factories) { + const api = registry.get(factory.implements); + expect(api).toBeDefined(); + } + }); +}); From b81ebe7e7219868add28e6aaa4a119d752b73b35 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 26 Apr 2020 14:11:08 +0200 Subject: [PATCH 5/9] packages/dev-utils: refactor to be more like createApp --- packages/dev-utils/src/devApp/render.tsx | 178 +++++++++++++---------- plugins/graphiql/dev/index.tsx | 47 +++--- 2 files changed, 127 insertions(+), 98 deletions(-) diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 1919afd8e0..f39a1ebb0e 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React, { FC, ComponentType } from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import BookmarkIcon from '@material-ui/icons/Bookmark'; @@ -31,94 +31,126 @@ import { ApiHolder, } from '@backstage/core'; import { lightTheme } from '@backstage/theme'; -import * as apiFactories from './apiFactories'; +import * as defaultApiFactories from './apiFactories'; +// TODO(rugvip): export proper plugin type from core that isn't the plugin class type BackstagePlugin = ReturnType; -export type RenderInAppConfig = { - plugins: BackstagePlugin[]; - apis: ApiFactory[]; -}; +// DevApp builder that is similar to the App builder API, but creates an App +// with the purpose of developing one or more plugins inside it. +class DevAppBuilder { + private readonly plugins = new Array(); + private readonly factories = new Array>(); -// 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); + // Register one or more plugins to render in the dev app + registerPlugin(...plugins: BackstagePlugin[]): DevAppBuilder { + this.plugins.push(...plugins); + return this; } - return registry; -} + // Register an API factory to add to the app + registerApiFactory( + factory: ApiFactory, + ): DevAppBuilder { + this.factories.push(factory); + return this; + } -// Create a sidebar that exposes the touchpoints of a plugin -function setupSidebar(config: RenderInAppConfig): JSX.Element { - const sidebarItems = new Array(); + // Build a DevApp component using the resources registered so far + build(): ComponentType<{}> { + const app = createApp(); + app.registerApis(this.setupApiRegistry(this.factories)); + app.registerPlugin(...this.plugins); + const AppComponent = app.build(); - for (const plugin of config.plugins) { - for (const output of plugin.output()) { - switch (output.type) { - case 'route': { - const { path } = output; - sidebarItems.push( - , - ); - break; + const sidebar = this.setupSidebar(this.plugins); + + const DevApp: FC<{}> = () => { + return ( + + + + + {sidebar} + + + + + + ); + }; + + return DevApp; + } + + // Build and render directory to #root element + render(): void { + const DevApp = this.build(); + ReactDOM.render(, document.getElementById('root')); + } + + // Create a sidebar that exposes the touchpoints of a plugin + private setupSidebar(plugins: BackstagePlugin[]): JSX.Element { + const sidebarItems = new Array(); + + for (const plugin of plugins) { + for (const output of plugin.output()) { + switch (output.type) { + case 'route': { + const { path } = output; + sidebarItems.push( + , + ); + break; + } + default: + break; } - default: - break; } } + + return ( + + + {sidebarItems} + + ); } - return ( - - - {sidebarItems} - - ); + // Set up an API registry that merges together default implementations with ones provided through config. + private setupApiRegistry( + providedFactories: ApiFactory[], + ): ApiHolder { + const providedApis = new Set( + providedFactories.map(factory => factory.implements), + ); + + // Exlude any default API factory that we receive a factory for in the config + const defaultFactories = Object.values( + defaultApiFactories, + ).filter(factory => providedApis.has(factory.implements)); + const allFactories = [...defaultFactories, ...providedFactories]; + + // 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; + } } // 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')); +// Creates a dev app for rendering one or more plugins and exposing the touchpoints of the plugin. +export function createDevApp() { + return new DevAppBuilder(); } diff --git a/plugins/graphiql/dev/index.tsx b/plugins/graphiql/dev/index.tsx index 0dc48f1924..32d18b4f0b 100644 --- a/plugins/graphiql/dev/index.tsx +++ b/plugins/graphiql/dev/index.tsx @@ -14,30 +14,27 @@ * limitations under the License. */ -import { renderPluginsInApp } from '@backstage/dev-utils'; +import { createDevApp } from '@backstage/dev-utils'; import { plugin, GraphQLEndpoints, graphQlBrowseApiRef } from '../src'; -import { createApiFactory } from '@backstage/core'; -renderPluginsInApp({ - plugins: [plugin], - apis: [ - createApiFactory({ - implements: graphQlBrowseApiRef, - deps: {}, - factory() { - return GraphQLEndpoints.from([ - GraphQLEndpoints.create({ - id: 'gitlab', - title: 'GitLab', - url: 'https://gitlab.com/api/graphql', - }), - GraphQLEndpoints.create({ - id: 'countries', - title: 'Countries', - url: 'https://countries.trevorblades.com/', - }), - ]); - }, - }), - ], -}); +createDevApp() + .registerPlugin(plugin) + .registerApiFactory({ + implements: graphQlBrowseApiRef, + deps: {}, + factory() { + return GraphQLEndpoints.from([ + GraphQLEndpoints.create({ + id: 'gitlab', + title: 'GitLab', + url: 'https://gitlab.com/api/graphql', + }), + GraphQLEndpoints.create({ + id: 'countries', + title: 'Countries', + url: 'https://countries.trevorblades.com/', + }), + ]); + }, + }) + .render(); From e62bd2b4313dec417322b35b635b7005249f6987 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 26 Apr 2020 14:22:58 +0200 Subject: [PATCH 6/9] plugins/home-page: use dev-utils to set up dev app --- plugins/home-page/dev/index.tsx | 40 ++++----------------------------- plugins/home-page/package.json | 1 + 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/plugins/home-page/dev/index.tsx b/plugins/home-page/dev/index.tsx index c68b8f7a51..d97643057b 100644 --- a/plugins/home-page/dev/index.tsx +++ b/plugins/home-page/dev/index.tsx @@ -14,41 +14,9 @@ * limitations under the License. */ -import React, { FC } from 'react'; -import ReactDOM from 'react-dom'; -import { BrowserRouter } from 'react-router-dom'; -import HomeIcon from '@material-ui/icons/Home'; -import { ThemeProvider, CssBaseline } from '@material-ui/core'; -import { - createApp, - SidebarPage, - Sidebar, - SidebarItem, - SidebarSpacer, -} from '@backstage/core'; -import { lightTheme } from '@backstage/theme'; +import { createDevApp } from '@backstage/dev-utils'; import { plugin } from '../src/plugin'; -const app = createApp(); -app.registerPlugin(plugin); -const AppComponent = app.build(); - -const App: FC<{}> = () => { - return ( - - - - - - - - - - - - - - ); -}; - -ReactDOM.render(, document.getElementById('root')); +createDevApp() + .registerPlugin(plugin) + .render(); diff --git a/plugins/home-page/package.json b/plugins/home-page/package.json index 6aed59a782..17c973bd35 100644 --- a/plugins/home-page/package.json +++ b/plugins/home-page/package.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.4", + "@backstage/dev-utils": "^0.1.1-alpha.4", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", From 6ab99a2052ce9b852e0fed697cecb44807eee5ae Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 26 Apr 2020 14:23:49 +0200 Subject: [PATCH 7/9] plugins/graphiql: move dev and test utils to devDependencies --- plugins/graphiql/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index d52957cf84..2db84d275a 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -27,6 +27,8 @@ }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.4", + "@backstage/dev-utils": "^0.1.1-alpha.4", + "@backstage/test-utils": "^0.1.1-alpha.4", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", @@ -38,8 +40,6 @@ }, "dependencies": { "@backstage/core": "^0.1.1-alpha.4", - "@backstage/dev-utils": "^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", From 5c797e4f5376e294b2cbd77ef97655fdaff50635 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 26 Apr 2020 14:30:58 +0200 Subject: [PATCH 8/9] packages/dev-utils: default to navigating to the first registered route --- packages/dev-utils/src/devApp/render.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index f39a1ebb0e..768e0973e6 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -86,6 +86,15 @@ class DevAppBuilder { // Build and render directory to #root element render(): void { const DevApp = this.build(); + + const paths = this.findPluginPaths(this.plugins); + + if (window.location.pathname === '/') { + if (!paths.includes('/') && paths.length > 0) { + window.location.pathname = paths[0]; + } + } + ReactDOM.render(, document.getElementById('root')); } @@ -145,6 +154,20 @@ class DevAppBuilder { return registry; } + + private findPluginPaths(plugins: BackstagePlugin[]) { + const paths = new Array(); + + for (const plugin of plugins) { + for (const output of plugin.output()) { + if (output.type === 'route') { + paths.push(output.path); + } + } + } + + return paths; + } } // TODO(rugvip): Figure out patterns for how to allow in-house apps to build upon From 99f9c6863eb3c006c05d24e25f469ee892434a17 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 28 Apr 2020 14:06:20 +0200 Subject: [PATCH 9/9] packages/dev-utils: use doc comments + text fix in README --- packages/core/src/api/apis/helpers.ts | 8 +++++--- packages/dev-utils/README.md | 2 +- packages/dev-utils/src/devApp/render.tsx | 26 +++++++++++++++++------- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/core/src/api/apis/helpers.ts b/packages/core/src/api/apis/helpers.ts index 1a46781c58..d8f8e8ac11 100644 --- a/packages/core/src/api/apis/helpers.ts +++ b/packages/core/src/api/apis/helpers.ts @@ -16,9 +16,11 @@ import { ApiFactory } from './types'; -// Used to infer types for a standalone ApiFactory that isn't immediately passed -// to another function. -// This function doesn't actually do anything, it's only used to infer types. +/** + * Used to infer types for a standalone ApiFactory that isn't immediately passed + * to another function. + * This function doesn't actually do anything, it's only used to infer types. + */ export function createApiFactory( factory: ApiFactory, ): ApiFactory { diff --git a/packages/dev-utils/README.md b/packages/dev-utils/README.md index d082b7d06b..04f8404f64 100644 --- a/packages/dev-utils/README.md +++ b/packages/dev-utils/README.md @@ -2,7 +2,7 @@ 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. +This package provides utilities that help in developing plugins for Backstage, like App wrappers and API implementations for standalone serving of apps. ## Installation diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 768e0973e6..9c9a72094c 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -36,19 +36,25 @@ import * as defaultApiFactories from './apiFactories'; // TODO(rugvip): export proper plugin type from core that isn't the plugin class type BackstagePlugin = ReturnType; -// DevApp builder that is similar to the App builder API, but creates an App -// with the purpose of developing one or more plugins inside it. +/** + * DevApp builder that is similar to the App builder API, but creates an App + * with the purpose of developing one or more plugins inside it. + */ class DevAppBuilder { private readonly plugins = new Array(); private readonly factories = new Array>(); - // Register one or more plugins to render in the dev app + /** + * Register one or more plugins to render in the dev app + */ registerPlugin(...plugins: BackstagePlugin[]): DevAppBuilder { this.plugins.push(...plugins); return this; } - // Register an API factory to add to the app + /** + * Register an API factory to add to the app + */ registerApiFactory( factory: ApiFactory, ): DevAppBuilder { @@ -56,7 +62,9 @@ class DevAppBuilder { return this; } - // Build a DevApp component using the resources registered so far + /** + * Build a DevApp component using the resources registered so far + */ build(): ComponentType<{}> { const app = createApp(); app.registerApis(this.setupApiRegistry(this.factories)); @@ -83,7 +91,9 @@ class DevAppBuilder { return DevApp; } - // Build and render directory to #root element + /** + * Build and render directory to #root element + */ render(): void { const DevApp = this.build(); @@ -173,7 +183,9 @@ class DevAppBuilder { // TODO(rugvip): Figure out patterns for how to allow in-house apps to build upon // this to provide their own plugin dev wrappers. -// Creates a dev app for rendering one or more plugins and exposing the touchpoints of the plugin. +/** + * Creates a dev app for rendering one or more plugins and exposing the touchpoints of the plugin. + */ export function createDevApp() { return new DevAppBuilder(); }