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