diff --git a/packages/app-next/src/examples/graphiqlPlugin.tsx b/packages/app-next/src/examples/graphiqlPlugin.tsx index 2984b522dc..4e3768dafa 100644 --- a/packages/app-next/src/examples/graphiqlPlugin.tsx +++ b/packages/app-next/src/examples/graphiqlPlugin.tsx @@ -29,5 +29,5 @@ export const GraphiqlPage = createPageExtension({ export const graphiqlPlugin = createPlugin({ id: 'graphiql', - defaultExtensions: [GraphiqlPage], + extensions: [GraphiqlPage], }); diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx index ee4130380f..77ff65f330 100644 --- a/packages/frontend-app-api/src/createApp.tsx +++ b/packages/frontend-app-api/src/createApp.tsx @@ -43,7 +43,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { // apply config to adjust default extension instances and add more const extensionParams = mergeExtensionParameters( [ - ...options.plugins.flatMap(plugin => plugin.defaultExtensions), + ...options.plugins.flatMap(plugin => plugin.extensions), ...builtinExtensions, ], readAppExtensionParameters(appConfig), @@ -116,8 +116,8 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { ); return ( <> - {rootComponents.map(Component => ( - + {rootComponents.map((Component, i) => ( + ))} ); diff --git a/packages/frontend-app-api/src/index.ts b/packages/frontend-app-api/src/index.ts index 45fe9413ba..9674eb345d 100644 --- a/packages/frontend-app-api/src/index.ts +++ b/packages/frontend-app-api/src/index.ts @@ -15,7 +15,7 @@ */ /** - * TODO + * Core API used by Backstage frontend apps. * * @packageDocumentation */ diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 3068b25437..84aa3396e8 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -19,15 +19,7 @@ export interface BackstagePlugin { // (undocumented) $$type: 'backstage-plugin'; // (undocumented) - defaultExtensions: Extension[]; - // (undocumented) - id: string; -} - -// @public (undocumented) -export interface BackstagePluginOptions { - // (undocumented) - defaultExtensions?: Extension[]; + extensions: Extension[]; // (undocumented) id: string; } @@ -121,7 +113,7 @@ export function createPageExtension< ): Extension; // @public (undocumented) -export function createPlugin(options: BackstagePluginOptions): BackstagePlugin; +export function createPlugin(options: PluginOptions): BackstagePlugin; // @public (undocumented) export function createSchemaFromZod( @@ -174,6 +166,14 @@ export type ExtensionDataValue = { [K in keyof TData]: TData[K]['T']; }; +// @public (undocumented) +export interface PluginOptions { + // (undocumented) + extensions?: Extension[]; + // (undocumented) + id: string; +} + // @public (undocumented) export type PortableSchema = { parse: (input: unknown) => TOutput; diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index cf353e55a6..0c420abbf3 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -24,7 +24,9 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@testing-library/jest-dom": "^5.10.1" + "@backstage/frontend-app-api": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3" }, "files": [ "dist" diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx new file mode 100644 index 0000000000..f1caaf7fdf --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 2023 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 React from 'react'; +import { PortableSchema } from '../createSchemaFromZod'; +import { coreExtensionData } from '../types'; +import { createPageExtension } from './createPageExtension'; + +describe('createPageExtension', () => { + it('creates the extension properly', () => { + const configSchema: PortableSchema<{ path: string }> = { + parse: jest.fn(), + schema: {} as any, + }; + + expect( + createPageExtension({ + id: 'test', + configSchema, + component: async () =>
, + }), + ).toEqual({ + $$type: 'extension', + id: 'test', + at: 'core.router/routes', + configSchema: expect.anything(), + disabled: false, + inputs: {}, + output: { + component: expect.anything(), + path: expect.anything(), + }, + factory: expect.any(Function), + }); + + expect( + createPageExtension({ + id: 'test', + at: 'other/place', + disabled: true, + configSchema, + inputs: { + first: { + extensionData: { component: coreExtensionData.reactComponent }, + }, + }, + component: async () =>
, + }), + ).toEqual({ + $$type: 'extension', + id: 'test', + at: 'other/place', + configSchema: expect.anything(), + disabled: true, + inputs: { + first: { + extensionData: { component: coreExtensionData.reactComponent }, + }, + }, + output: { + component: expect.anything(), + path: expect.anything(), + }, + factory: expect.any(Function), + }); + + expect( + createPageExtension({ + id: 'test', + defaultPath: '/here', + component: async () =>
, + }), + ).toEqual({ + $$type: 'extension', + id: 'test', + at: 'core.router/routes', + configSchema: expect.anything(), + disabled: false, + inputs: {}, + output: { + component: expect.anything(), + path: expect.anything(), + }, + factory: expect.any(Function), + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx similarity index 96% rename from packages/frontend-plugin-api/src/createPageExtension.tsx rename to packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index de2ca9f343..22b3df318d 100644 --- a/packages/frontend-plugin-api/src/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -15,14 +15,14 @@ */ import React from 'react'; -import { createSchemaFromZod, PortableSchema } from './createSchemaFromZod'; +import { createSchemaFromZod, PortableSchema } from '../createSchemaFromZod'; import { AnyExtensionDataMap, coreExtensionData, createExtension, Extension, ExtensionDataValue, -} from './types'; +} from '../types'; /** * Helper for creating extensions for a routable React page component. diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts new file mode 100644 index 0000000000..d2a18fe977 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 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. + */ + +export { createPageExtension } from './createPageExtension'; diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index df30cc813d..a9c4a4f2e7 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -15,26 +15,24 @@ */ /** - * TODO + * Core API used by Backstage frontend plugins. * * @packageDocumentation */ -export { createPageExtension } from './createPageExtension'; export { createSchemaFromZod, type PortableSchema, } from './createSchemaFromZod'; +export * from './extensions'; export { coreExtensionData, createExtension, - createPlugin, type AnyExtensionDataMap, - type BackstagePlugin, - type BackstagePluginOptions, type CreateExtensionOptions, type Extension, type ExtensionDataBind, type ExtensionDataRef, type ExtensionDataValue, } from './types'; +export * from './wiring'; diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts index d634dc3c5d..2affa4ae42 100644 --- a/packages/frontend-plugin-api/src/types.ts +++ b/packages/frontend-plugin-api/src/types.ts @@ -25,6 +25,7 @@ export type ExtensionDataRef = { }; /** @public */ +// TODO: change to options object with ID. export function createExtensionDataRef(id: string): ExtensionDataRef { return { id, $$type: 'extension-data' } as ExtensionDataRef; } @@ -100,25 +101,3 @@ export function createExtension< inputs: options.inputs ?? {}, }; } - -/** @public */ -export interface BackstagePluginOptions { - id: string; - defaultExtensions?: Extension[]; -} - -/** @public */ -export interface BackstagePlugin { - $$type: 'backstage-plugin'; - id: string; - defaultExtensions: Extension[]; -} - -/** @public */ -export function createPlugin(options: BackstagePluginOptions): BackstagePlugin { - return { - ...options, - $$type: 'backstage-plugin', - defaultExtensions: options.defaultExtensions ?? [], - }; -} diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts new file mode 100644 index 0000000000..36389d7e63 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -0,0 +1,191 @@ +/* + * Copyright 2023 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 React from 'react'; +import { createApp } from '@backstage/frontend-app-api'; +import { render, screen } from '@testing-library/react'; +import { createSchemaFromZod } from '../createSchemaFromZod'; +import { + createExtension, + coreExtensionData, + createExtensionDataRef, +} from '../types'; +import { createPlugin, BackstagePlugin } from './createPlugin'; +import { JsonObject } from '@backstage/types'; + +const nameExtensionDataRef = createExtensionDataRef('name'); + +const TechRadarPage = createExtension({ + id: 'plugin.techradar.page', + at: 'test.output/names', + output: { + name: nameExtensionDataRef, + }, + factory({ bind }) { + bind.name('TechRadar'); + }, +}); + +const CatalogPage = createExtension({ + id: 'plugin.catalog.page', + at: 'test.output/names', + output: { + name: nameExtensionDataRef, + }, + configSchema: createSchemaFromZod(z => + z.object({ name: z.string().default('Catalog') }), + ), + factory({ bind, config }) { + bind.name(config.name); + }, +}); + +const TechDocsAddon = createExtension({ + id: 'plugin.techdocs.addon.example', + at: 'plugin.techdocs.page/addons', + output: { + name: nameExtensionDataRef, + }, + configSchema: createSchemaFromZod(z => + z.object({ name: z.string().default('TechDocsAddon') }), + ), + factory({ bind, config }) { + bind.name(config.name); + }, +}); + +const TechDocsPage = createExtension({ + id: 'plugin.techdocs.page', + at: 'test.output/names', + inputs: { + addons: { + extensionData: { + name: nameExtensionDataRef, + }, + }, + }, + output: { + name: nameExtensionDataRef, + }, + factory({ bind, inputs }) { + bind.name(`TechDocs-${inputs.addons.map(n => n.name).join('-')}`); + }, +}); + +const outputExtension = createExtension({ + id: 'test.output', + at: 'root', + inputs: { + names: { + extensionData: { + name: nameExtensionDataRef, + }, + }, + }, + output: { + component: coreExtensionData.reactComponent, + }, + factory({ bind, inputs }) { + bind.component(() => + React.createElement('span', {}, [ + `Names: ${inputs.names.map(n => n.name).join(', ')}`, + ]), + ); + }, +}); + +function createTestAppRoot({ + plugins, + config = {}, +}: { + plugins: BackstagePlugin[]; + config: JsonObject; +}) { + Object.defineProperty(process.env, 'APP_CONFIG', { + value: [ + { + data: config, + context: 'test', + }, + ], + configurable: true, + }); + + return createApp({ plugins: plugins }).createRoot(); +} + +describe('createPlugin', () => { + it('should create an empty plugin', () => { + const plugin = createPlugin({ id: 'empty' }); + + expect(plugin).toBeDefined(); + }); + + it('should create a plugin with extension instances', () => { + const plugin = createPlugin({ + id: 'empty', + extensions: [TechRadarPage, CatalogPage, outputExtension], + }); + expect(plugin).toBeDefined(); + + render( + createTestAppRoot({ + plugins: [plugin], + config: { app: { extensions: [{ 'core.router': false }] } }, + }), + ); + + expect(screen.getByText('Names: TechRadar, Catalog')).toBeInTheDocument(); + }); + + it('should create a plugin with nested extension instances', () => { + const plugin = createPlugin({ + id: 'empty', + extensions: [ + TechRadarPage, + CatalogPage, + TechDocsPage, + TechDocsAddon, + outputExtension, + ], + }); + expect(plugin).toBeDefined(); + + render( + createTestAppRoot({ + plugins: [plugin], + config: { + app: { + extensions: [ + { 'core.router': false }, + { + 'plugin.catalog.page': { + config: { name: 'CatalogRenamed' }, + }, + }, + ], + }, + }, + }), + ); + + expect( + screen.getByText( + 'Names: TechRadar, CatalogRenamed, TechDocs-TechDocsAddon', + ), + ).toBeInTheDocument(); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts new file mode 100644 index 0000000000..3e15fb0b19 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2023 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 { Extension } from '../types'; + +/** @public */ +export interface PluginOptions { + id: string; + extensions?: Extension[]; +} + +/** @public */ +export interface BackstagePlugin { + $$type: 'backstage-plugin'; + id: string; + extensions: Extension[]; +} + +/** @public */ +export function createPlugin(options: PluginOptions): BackstagePlugin { + return { + ...options, + $$type: 'backstage-plugin', + extensions: options.extensions ?? [], + }; +} diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts new file mode 100644 index 0000000000..d96ba09971 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 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. + */ + +export { + createPlugin, + type BackstagePlugin, + type PluginOptions, +} from './createPlugin'; diff --git a/yarn.lock b/yarn.lock index 9f7604ede8..22fcc1a57d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4298,8 +4298,10 @@ __metadata: resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api" dependencies: "@backstage/cli": "workspace:^" + "@backstage/frontend-app-api": "workspace:^" "@backstage/types": "workspace:^" "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 lodash: ^4.17.21 zod: ^3.21.4 zod-to-json-schema: ^3.21.4