diff --git a/plugins/scaffolder/src/next/Router/Router.test.tsx b/plugins/scaffolder/src/next/Router/Router.test.tsx
new file mode 100644
index 0000000000..5094c786e0
--- /dev/null
+++ b/plugins/scaffolder/src/next/Router/Router.test.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 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 { TemplateListPage } from '../TemplateListPage';
+import { TemplateWizardPage } from '../TemplateWizardPage';
+import { Router } from './Router';
+import { renderInTestApp } from '@backstage/test-utils';
+import {
+ createScaffolderFieldExtension,
+ ScaffolderFieldExtensions,
+} from '../../extensions';
+import { scaffolderPlugin } from '../../plugin';
+
+jest.mock('../TemplateListPage', () => ({
+ TemplateListPage: jest.fn(() => null),
+}));
+
+jest.mock('../TemplateWizardPage', () => ({
+ TemplateWizardPage: jest.fn(() => null),
+}));
+
+describe('Router', () => {
+ beforeEach(() => {
+ (TemplateWizardPage as jest.Mock).mockClear();
+ (TemplateListPage as jest.Mock).mockClear();
+ });
+ describe('/', () => {
+ it('should render the TemplateListPage', async () => {
+ await renderInTestApp();
+
+ expect(TemplateListPage).toHaveBeenCalled();
+ });
+ });
+
+ describe('/templates/:templateName', () => {
+ it('should render the TemplateWizard page', async () => {
+ await renderInTestApp(, { routeEntries: ['/templates/foo'] });
+
+ expect(TemplateWizardPage).toHaveBeenCalled();
+ });
+
+ it('should extract the fieldExtensions and pass them through', async () => {
+ const mockComponent = () => null;
+ const CustomFieldExtension = scaffolderPlugin.provide(
+ createScaffolderFieldExtension({
+ name: 'custom',
+ component: mockComponent,
+ }),
+ );
+
+ await renderInTestApp(
+
+
+
+
+ ,
+ { routeEntries: ['/templates/foo'] },
+ );
+
+ const mock = TemplateWizardPage as jest.Mock;
+ const [{ customFieldExtensions }] = mock.mock.calls[0];
+
+ expect(customFieldExtensions).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ name: 'custom', component: mockComponent }),
+ ]),
+ );
+ });
+ });
+});
diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx
new file mode 100644
index 0000000000..8ab9755ab2
--- /dev/null
+++ b/plugins/scaffolder/src/next/Router/Router.tsx
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2022 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, { PropsWithChildren } from 'react';
+import { Routes, Route, useOutlet } from 'react-router';
+import { TemplateListPage } from '../TemplateListPage';
+import { SecretsContextProvider } from '../TemplateWizardPage/SecretsContext';
+import { TemplateWizardPage } from '../TemplateWizardPage';
+import {
+ FieldExtensionOptions,
+ FIELD_EXTENSION_WRAPPER_KEY,
+ FIELD_EXTENSION_KEY,
+ DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS,
+} from '../../extensions';
+
+import { useElementFilter } from '@backstage/core-plugin-api';
+import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
+
+export type RouterProps = {
+ TemplateCardComponent?: React.ComponentType<{
+ template: TemplateEntityV1beta3;
+ }>;
+};
+
+export const Router = (props: PropsWithChildren) => {
+ const { TemplateCardComponent } = props;
+
+ const outlet = useOutlet() || props.children;
+
+ const customFieldExtensions = useElementFilter(outlet, elements =>
+ elements
+ .selectByComponentData({
+ key: FIELD_EXTENSION_WRAPPER_KEY,
+ })
+ .findComponentData({
+ key: FIELD_EXTENSION_KEY,
+ }),
+ );
+
+ const fieldExtensions = [
+ ...customFieldExtensions,
+ ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter(
+ ({ name }) =>
+ !customFieldExtensions.some(
+ customFieldExtension => customFieldExtension.name === name,
+ ),
+ ),
+ ];
+
+ return (
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+ );
+};
diff --git a/plugins/scaffolder/src/next/Router/index.ts b/plugins/scaffolder/src/next/Router/index.ts
new file mode 100644
index 0000000000..5ad2ae6e6a
--- /dev/null
+++ b/plugins/scaffolder/src/next/Router/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2022 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 { Router } from './Router';