diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx
index b57c680474..25d1aacf8f 100644
--- a/packages/app-next/src/App.tsx
+++ b/packages/app-next/src/App.tsx
@@ -20,6 +20,7 @@ import { pagesPlugin } from './examples/pagesPlugin';
import notFoundErrorPage from './examples/notFoundErrorPageExtension';
import userSettingsPlugin from '@backstage/plugin-user-settings/alpha';
import homePlugin, {
+ extensions,
titleExtensionDataRef,
} from '@backstage/plugin-home/alpha';
@@ -74,13 +75,46 @@ TODO:
/* app.tsx */
-const homePageExtension = createExtension({
- name: 'myhomepage',
- attachTo: { id: 'page:home', input: 'props' },
- output: {
- children: coreExtensionData.reactElement,
- title: titleExtensionDataRef,
- },
+// const homePageExtension = createExtension({
+// name: 'myhomepage',
+// attachTo: { id: 'page:home', input: 'props' },
+// output: {
+// children: coreExtensionData.reactElement,
+// title: titleExtensionDataRef,
+// },
+// factory() {
+// return { children: homePage, title: 'just a title' };
+// },
+// });
+
+// const homePage = createPageExtension({
+// defaultPath: '/home',
+// routeRef: rootRouteRef,
+// inputs: {
+// props: createExtensionInput(
+// {
+// children: coreExtensionData.reactElement.optional(),
+// title: titleExtensionDataRef.optional(),
+// },
+
+// {
+// singleton: true,
+// optional: true,
+// },
+// ),
+// },
+// loader: ({ inputs }) =>
+// import('./components/').then(m =>
+// compatWrapper(
+// ,
+// ),
+// ),
+// });
+
+const homePageExtension = extensions.homePage.override({
factory() {
return { children: homePage, title: 'just a title' };
},
diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
index cd434a3ffc..b10901f1bd 100644
--- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
@@ -22,6 +22,9 @@ import {
createExtension,
ResolvedExtensionInputs,
AnyExtensionInputMap,
+ createExtensionBlueprint,
+ ExtensionInput,
+ AnyExtensionDataRef,
} from '../wiring';
import { RouteRef } from '../routing';
import { Expand } from '../types';
@@ -95,3 +98,131 @@ export function createPageExtension<
},
});
}
+
+export function createNewPageExtension<
+ TConfig extends { path: string },
+ TInputs extends {
+ [inputName in string]: ExtensionInput<
+ AnyExtensionDataRef,
+ { optional: boolean; singleton: boolean }
+ >;
+ },
+>(
+ options: (
+ | {
+ defaultPath: string;
+ }
+ | {
+ configSchema: PortableSchema;
+ }
+ ) & {
+ namespace?: string;
+ name?: string;
+ attachTo?: { id: string; input: string };
+ disabled?: boolean;
+ inputs?: TInputs;
+ routeRef?: RouteRef;
+ loader: (options: {
+ config: TConfig;
+ inputs: Expand>;
+ }) => Promise;
+ },
+): ExtensionDefinition {
+ const configSchema =
+ 'configSchema' in options
+ ? options.configSchema
+ : (createSchemaFromZod(z =>
+ z.object({ path: z.string().default(options.defaultPath) }),
+ ) as PortableSchema);
+
+ return createExtension({
+ kind: 'page',
+ namespace: options.namespace,
+ name: options.name,
+ attachTo: options.attachTo ?? { id: 'app/routes', input: 'routes' },
+ configSchema,
+ inputs: options.inputs,
+ disabled: options.disabled,
+ output: [
+ coreExtensionData.routePath,
+ coreExtensionData.reactElement,
+ coreExtensionData.routeRef.optional(),
+ ],
+ factory({ config, inputs, node }) {
+ const ExtensionComponent = lazy(() =>
+ options
+ .loader({ config, inputs })
+ .then(element => ({ default: () => element })),
+ );
+
+ const outputs = [
+ coreExtensionData.routePath(config.path),
+ coreExtensionData.reactElement(
+
+
+ ,
+ ),
+ ];
+
+ if (options.routeRef) {
+ return [...outputs, coreExtensionData.routeRef(options.routeRef)];
+ }
+
+ return outputs;
+ },
+ });
+}
+/**
+ * A blueprint for creating extensions for routable React page components.
+ * @public
+ */
+export const PageExtensionBlueprint = createExtensionBlueprint({
+ kind: 'page',
+ attachTo: { id: 'app/routes', input: 'routes' },
+ output: [
+ coreExtensionData.routePath,
+ coreExtensionData.reactElement,
+ coreExtensionData.routeRef.optional(),
+ ],
+ config: {
+ schema: {
+ path: z => z.string().optional(),
+ },
+ },
+ factory(
+ {
+ defaultPath,
+ loader,
+ routeRef,
+ }: {
+ defaultPath?: string;
+ // TODO(blam) This type is impossible to type properly here as we don't have access
+ // to the input type generic. Maybe not a deal breaker though.
+ // It means we have to override the factory function in the `.make` method instead.
+ loader: (opts: unknown) => Promise;
+ routeRef?: RouteRef;
+ },
+ { config, inputs, node },
+ ) {
+ const ExtensionComponent = lazy(() =>
+ loader({ config, inputs }).then(element => ({ default: () => element })),
+ );
+
+ // TODO(blam): this is a little awkward for optional returns. I wonder if we should be using generators or yield instead
+ // for a better API here.
+ const outputs = [
+ coreExtensionData.routePath(config.path ?? defaultPath!),
+ coreExtensionData.reactElement(
+
+
+ ,
+ ),
+ ];
+
+ if (routeRef) {
+ return [...outputs, coreExtensionData.routeRef(routeRef)];
+ }
+
+ return outputs;
+ },
+});
diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx
index b13c2a2e0e..109eea333e 100644
--- a/plugins/home/src/alpha.tsx
+++ b/plugins/home/src/alpha.tsx
@@ -25,6 +25,10 @@ import {
createRouteRef,
} from '@backstage/frontend-plugin-api';
import { compatWrapper } from '@backstage/core-compat-api';
+import {
+ PageExtensionBlueprint,
+ createNewPageExtension,
+} from '@backstage/frontend-plugin-api/src/extensions/createPageExtension';
const rootRouteRef = createRouteRef();
@@ -35,15 +39,15 @@ export const titleExtensionDataRef = createExtensionDataRef().with({
id: 'title',
});
-const homePage = createPageExtension({
+const homePage = createNewPageExtension({
defaultPath: '/home',
routeRef: rootRouteRef,
inputs: {
props: createExtensionInput(
- {
- children: coreExtensionData.reactElement.optional(),
- title: titleExtensionDataRef.optional(),
- },
+ [
+ coreExtensionData.reactElement.optional(),
+ titleExtensionDataRef.optional(),
+ ],
{
singleton: true,
@@ -55,13 +59,53 @@ const homePage = createPageExtension({
import('./components/').then(m =>
compatWrapper(
,
),
),
});
+homePage.override({
+ factory() {},
+});
+
+// const homePage2 = PageExtensionBlueprint.make({
+// inputs: {
+// props: createExtensionInput(
+// [
+// coreExtensionData.reactElement.optional(),
+// titleExtensionDataRef.optional(),
+// ],
+// {
+// singleton: true,
+// optional: true,
+// },
+// ),
+// },
+// factory(origFactory, { config, inputs, node }) {
+// return origFactory({
+// defaultPath: '/home',
+// routeRef: rootRouteRef,
+// loader: () =>
+// import('./components/').then(m =>
+// compatWrapper(
+// ,
+// ),
+// },
+// });
+
+/**
+ * @alpha
+ * This will not be the way to export extensions eventually,
+ * will be something like `homePlugin.extensions.homePage` or `homePlugin.extensions.get('page')`
+ * Just haven't worked out a nice way to fix the types yet
+ */
+export const extensions = { homePage };
+
/**
* @alpha
*/