diff --git a/packages/app-defaults/.eslintrc.js b/packages/app-defaults/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/packages/app-defaults/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/packages/app-defaults/README.md b/packages/app-defaults/README.md
new file mode 100644
index 0000000000..15a0bd3d24
--- /dev/null
+++ b/packages/app-defaults/README.md
@@ -0,0 +1,17 @@
+# @backstage/app-defaults
+
+This package provides a default wiring of a Backstage app that avoids boilerplate when setting up a standard Backstage app.
+
+## Installation
+
+Install the package via Yarn:
+
+```sh
+cd packages/app
+yarn add @backstage/app-defaults
+```
+
+## Documentation
+
+- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
+- [Backstage Documentation](https://backstage.io/docs)
diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json
new file mode 100644
index 0000000000..c7594470d6
--- /dev/null
+++ b/packages/app-defaults/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "@backstage/app-defaults",
+ "description": "Provides the default wiring of a Backstage App",
+ "version": "0.1.0",
+ "private": false,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "packages/app-defaults"
+ },
+ "keywords": [
+ "backstage"
+ ],
+ "license": "Apache-2.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "scripts": {
+ "build": "backstage-cli build --outputs types,esm",
+ "lint": "backstage-cli lint",
+ "test": "backstage-cli test",
+ "prepack": "backstage-cli prepack",
+ "postpack": "backstage-cli postpack",
+ "clean": "backstage-cli clean"
+ },
+ "dependencies": {
+ "@backstage/core-components": "^0.7.1",
+ "@backstage/config": "^0.1.10",
+ "@backstage/core-plugin-api": "^0.1.11",
+ "@backstage/theme": "^0.2.11",
+ "@backstage/types": "^0.1.1",
+ "@backstage/version-bridge": "^0.1.0",
+ "@material-ui/core": "^4.12.2",
+ "@material-ui/icons": "^4.9.1",
+ "@types/react": "*",
+ "@types/prop-types": "^15.7.3",
+ "prop-types": "^15.7.2",
+ "react": "^16.12.0",
+ "react-router-dom": "6.0.0-beta.0",
+ "react-use": "^17.2.4",
+ "zen-observable": "^0.8.15"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.8.0",
+ "@backstage/test-utils": "^0.1.19",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^11.2.5",
+ "@testing-library/react-hooks": "^7.0.2",
+ "@testing-library/user-event": "^13.1.8",
+ "@types/jest": "^26.0.7",
+ "@types/node": "^14.14.32",
+ "@types/zen-observable": "^0.8.0",
+ "cross-fetch": "^3.0.6",
+ "msw": "^0.29.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/packages/core-components/src/appDefaults/AppThemeProvider.tsx b/packages/app-defaults/src/createApp/AppThemeProvider.tsx
similarity index 100%
rename from packages/core-components/src/appDefaults/AppThemeProvider.tsx
rename to packages/app-defaults/src/createApp/AppThemeProvider.tsx
diff --git a/packages/app-defaults/src/createApp/createApp.test.tsx b/packages/app-defaults/src/createApp/createApp.test.tsx
new file mode 100644
index 0000000000..0bf90987db
--- /dev/null
+++ b/packages/app-defaults/src/createApp/createApp.test.tsx
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2020 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 { screen } from '@testing-library/react';
+import { renderWithEffects } from '@backstage/test-utils';
+import React, { PropsWithChildren } from 'react';
+import { MemoryRouter } from 'react-router-dom';
+import { defaultConfigLoader, createApp } from './createApp';
+
+(process as any).env = { NODE_ENV: 'test' };
+const anyEnv = process.env as any;
+const anyWindow = window as any;
+
+describe('defaultConfigLoader', () => {
+ afterEach(() => {
+ delete anyEnv.APP_CONFIG;
+ delete anyWindow.__APP_CONFIG__;
+ });
+
+ it('loads static config', async () => {
+ anyEnv.APP_CONFIG = [
+ { data: { my: 'config' }, context: 'a' },
+ { data: { my: 'override-config' }, context: 'b' },
+ ];
+
+ const configs = await defaultConfigLoader();
+ expect(configs).toEqual([
+ { data: { my: 'config' }, context: 'a' },
+ { data: { my: 'override-config' }, context: 'b' },
+ ]);
+ });
+
+ it('loads runtime config', async () => {
+ anyEnv.APP_CONFIG = [
+ { data: { my: 'override-config' }, context: 'a' },
+ { data: { my: 'config' }, context: 'b' },
+ ];
+
+ const configs = await (defaultConfigLoader as any)(
+ '{"my":"runtime-config"}',
+ );
+ expect(configs).toEqual([
+ { data: { my: 'override-config' }, context: 'a' },
+ { data: { my: 'config' }, context: 'b' },
+ { data: { my: 'runtime-config' }, context: 'env' },
+ ]);
+ });
+
+ it('fails to load invalid missing config', async () => {
+ await expect(defaultConfigLoader()).rejects.toThrow(
+ 'No static configuration provided',
+ );
+ });
+
+ it('fails to load invalid static config', async () => {
+ anyEnv.APP_CONFIG = { my: 'invalid-config' };
+ await expect(defaultConfigLoader()).rejects.toThrow(
+ 'Static configuration has invalid format',
+ );
+ });
+
+ it('fails to load bad runtime config', async () => {
+ anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }];
+
+ await expect((defaultConfigLoader as any)('}')).rejects.toThrow(
+ 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0',
+ );
+ });
+
+ it('loads config from window.__APP_CONFIG__', async () => {
+ anyEnv.APP_CONFIG = [
+ { data: { my: 'config' }, context: 'a' },
+ { data: { my: 'override-config' }, context: 'b' },
+ ];
+ const windowConfig = { app: { configKey: 'config-value' } };
+ anyWindow.__APP_CONFIG__ = windowConfig;
+
+ const configs = await defaultConfigLoader();
+
+ expect(configs).toEqual([
+ ...anyEnv.APP_CONFIG,
+ { context: 'window', data: windowConfig },
+ ]);
+ });
+});
+
+describe('Optional ThemeProvider', () => {
+ it('should render app with user-provided ThemeProvider', async () => {
+ const components = {
+ NotFoundErrorPage: () => null,
+ BootErrorPage: () => null,
+ Progress: () => null,
+ Router: MemoryRouter,
+ ErrorBoundaryFallback: () => null,
+ ThemeProvider: ({ children }: PropsWithChildren<{}>) => (
+ {children}
+ ),
+ };
+
+ const App = createApp({ components }).getProvider();
+
+ await renderWithEffects();
+
+ expect(screen.getByRole('main')).toBeInTheDocument();
+ });
+});
diff --git a/packages/app-defaults/src/createApp/createApp.tsx b/packages/app-defaults/src/createApp/createApp.tsx
new file mode 100644
index 0000000000..c3f477035f
--- /dev/null
+++ b/packages/app-defaults/src/createApp/createApp.tsx
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2020 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 { AppConfig } from '@backstage/config';
+import { JsonObject } from '@backstage/types';
+import { withDefaults } from '@backstage/core-components';
+import { PrivateAppImpl } from './App';
+import { AppComponents, AppConfigLoader, AppOptions } from './types';
+import { defaultApis } from './defaultApis';
+import { BackstagePlugin } from '@backstage/core-plugin-api';
+
+const REQUIRED_APP_COMPONENTS: Array = [
+ 'Progress',
+ 'Router',
+ 'NotFoundErrorPage',
+ 'BootErrorPage',
+ 'ErrorBoundaryFallback',
+];
+
+/**
+ * The default config loader, which expects that config is available at compile-time
+ * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as
+ * returned by the config loader.
+ *
+ * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string,
+ * which can be rewritten at runtime to contain an additional JSON config object.
+ * If runtime config is present, it will be placed first in the config array, overriding
+ * other config values.
+ *
+ * @public
+ */
+export const defaultConfigLoader: AppConfigLoader = async (
+ // This string may be replaced at runtime to provide additional config.
+ // It should be replaced by a JSON-serialized config object.
+ // It's a param so we can test it, but at runtime this will always fall back to default.
+ runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__',
+) => {
+ const appConfig = process.env.APP_CONFIG;
+ if (!appConfig) {
+ throw new Error('No static configuration provided');
+ }
+ if (!Array.isArray(appConfig)) {
+ throw new Error('Static configuration has invalid format');
+ }
+ const configs = appConfig.slice() as unknown as AppConfig[];
+
+ // Avoiding this string also being replaced at runtime
+ if (
+ runtimeConfigJson !==
+ '__app_injected_runtime_config__'.toLocaleUpperCase('en-US')
+ ) {
+ try {
+ const data = JSON.parse(runtimeConfigJson) as JsonObject;
+ if (Array.isArray(data)) {
+ configs.push(...data);
+ } else {
+ configs.push({ data, context: 'env' });
+ }
+ } catch (error) {
+ throw new Error(`Failed to load runtime configuration, ${error}`);
+ }
+ }
+
+ const windowAppConfig = (window as any).__APP_CONFIG__;
+ if (windowAppConfig) {
+ configs.push({
+ context: 'window',
+ data: windowAppConfig,
+ });
+ }
+ return configs;
+};
+
+/**
+ * Creates a new Backstage App.
+ *
+ * @public
+ */
+export function createApp(options?: AppOptions) {
+ const optionsWithDefaults = withDefaults(options);
+
+ const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter(
+ name => !options?.components?.[name],
+ );
+ if (missingRequiredComponents.length > 0) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'DEPRECATION WARNING: The createApp options will soon require a minimal set of components to ' +
+ 'be provided. You can use the default components by using withDefaults from @backstage/core-components ' +
+ 'like this: createApp(withDefaults({ ... })), or you can provide the components yourself. ' +
+ `The following components are missing: ${missingRequiredComponents.join(
+ ', ',
+ )}`,
+ );
+ }
+
+ const providedIconKeys = Object.keys(options?.icons ?? {});
+ const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter(
+ key => !providedIconKeys.includes(key),
+ );
+ if (missingIconKeys.length > 0) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'DEPRECATION WARNING: The createApp options will soon require a minimal set of icons to ' +
+ 'be provided. You can use the default icons by using withDefaults from @backstage/core-components ' +
+ 'like this: createApp(withDefaults({ ... })), or you can provide the icons yourself. ' +
+ `The following icons are missing: ${missingIconKeys.join(', ')}`,
+ );
+ }
+
+ if (!options?.themes) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'DEPRECATION WARNING: The createApp options will soon require the themes to be provided. ' +
+ 'You can use the default themes by using withDefaults from @backstage/core-components ' +
+ 'like this: createApp(withDefaults({ ... })), or you can provide the themes yourself. ',
+ );
+ }
+
+ const { icons, themes, components } = optionsWithDefaults;
+
+ return new PrivateAppImpl({
+ icons: icons!,
+ themes: themes!,
+ components: components! as AppComponents,
+ defaultApis,
+ apis: options?.apis ?? [],
+ bindRoutes: options?.bindRoutes,
+ plugins: (options?.plugins as BackstagePlugin[]) ?? [],
+ configLoader: options?.configLoader ?? defaultConfigLoader,
+ });
+}
diff --git a/packages/app-defaults/src/createApp/defaultApis.ts b/packages/app-defaults/src/createApp/defaultApis.ts
new file mode 100644
index 0000000000..fb02274cf0
--- /dev/null
+++ b/packages/app-defaults/src/createApp/defaultApis.ts
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2020 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 {
+ AlertApiForwarder,
+ NoOpAnalyticsApi,
+ ErrorApiForwarder,
+ ErrorAlerter,
+ GoogleAuth,
+ GithubAuth,
+ OAuth2,
+ OktaAuth,
+ GitlabAuth,
+ Auth0Auth,
+ MicrosoftAuth,
+ BitbucketAuth,
+ OAuthRequestManager,
+ WebStorage,
+ UrlPatternDiscovery,
+ SamlAuth,
+ OneLoginAuth,
+ UnhandledErrorForwarder,
+ AtlassianAuth,
+} from '../apis';
+
+import {
+ createApiFactory,
+ alertApiRef,
+ analyticsApiRef,
+ errorApiRef,
+ discoveryApiRef,
+ oauthRequestApiRef,
+ googleAuthApiRef,
+ githubAuthApiRef,
+ oauth2ApiRef,
+ oktaAuthApiRef,
+ gitlabAuthApiRef,
+ auth0AuthApiRef,
+ microsoftAuthApiRef,
+ storageApiRef,
+ configApiRef,
+ samlAuthApiRef,
+ oneloginAuthApiRef,
+ oidcAuthApiRef,
+ bitbucketAuthApiRef,
+ atlassianAuthApiRef,
+} from '@backstage/core-plugin-api';
+
+import OAuth2Icon from '@material-ui/icons/AcUnit';
+
+export const defaultApis = [
+ createApiFactory({
+ api: discoveryApiRef,
+ deps: { configApi: configApiRef },
+ factory: ({ configApi }) =>
+ UrlPatternDiscovery.compile(
+ `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`,
+ ),
+ }),
+ createApiFactory(alertApiRef, new AlertApiForwarder()),
+ createApiFactory(analyticsApiRef, new NoOpAnalyticsApi()),
+ createApiFactory({
+ api: errorApiRef,
+ deps: { alertApi: alertApiRef },
+ factory: ({ alertApi }) => {
+ const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder());
+ UnhandledErrorForwarder.forward(errorApi, { hidden: false });
+ return errorApi;
+ },
+ }),
+ createApiFactory({
+ api: storageApiRef,
+ deps: { errorApi: errorApiRef },
+ factory: ({ errorApi }) => WebStorage.create({ errorApi }),
+ }),
+ createApiFactory(oauthRequestApiRef, new OAuthRequestManager()),
+ createApiFactory({
+ api: googleAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ GoogleAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: microsoftAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ MicrosoftAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: githubAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ GithubAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ defaultScopes: ['read:user'],
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: oktaAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ OktaAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: gitlabAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ GitlabAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: auth0AuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ Auth0Auth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: oauth2ApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ OAuth2.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: samlAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, configApi }) =>
+ SamlAuth.create({
+ discoveryApi,
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: oneloginAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ OneLoginAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: oidcAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ OAuth2.create({
+ discoveryApi,
+ oauthRequestApi,
+ provider: {
+ id: 'oidc',
+ title: 'Your Identity Provider',
+ icon: OAuth2Icon,
+ },
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: bitbucketAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ BitbucketAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ defaultScopes: ['team'],
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+ }),
+ createApiFactory({
+ api: atlassianAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) => {
+ return AtlassianAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getOptionalString('auth.environment'),
+ });
+ },
+ }),
+];
diff --git a/packages/core-components/src/appDefaults/defaultAppComponents.test.tsx b/packages/app-defaults/src/createApp/defaultAppComponents.test.tsx
similarity index 100%
rename from packages/core-components/src/appDefaults/defaultAppComponents.test.tsx
rename to packages/app-defaults/src/createApp/defaultAppComponents.test.tsx
diff --git a/packages/core-components/src/appDefaults/defaultAppComponents.tsx b/packages/app-defaults/src/createApp/defaultAppComponents.tsx
similarity index 100%
rename from packages/core-components/src/appDefaults/defaultAppComponents.tsx
rename to packages/app-defaults/src/createApp/defaultAppComponents.tsx
diff --git a/packages/core-components/src/appDefaults/defaultAppIcons.tsx b/packages/app-defaults/src/createApp/defaultAppIcons.tsx
similarity index 100%
rename from packages/core-components/src/appDefaults/defaultAppIcons.tsx
rename to packages/app-defaults/src/createApp/defaultAppIcons.tsx
diff --git a/packages/core-components/src/appDefaults/defaultAppThemes.tsx b/packages/app-defaults/src/createApp/defaultAppThemes.tsx
similarity index 100%
rename from packages/core-components/src/appDefaults/defaultAppThemes.tsx
rename to packages/app-defaults/src/createApp/defaultAppThemes.tsx
diff --git a/packages/app-defaults/src/createApp/icons.tsx b/packages/app-defaults/src/createApp/icons.tsx
new file mode 100644
index 0000000000..df1e69da6a
--- /dev/null
+++ b/packages/app-defaults/src/createApp/icons.tsx
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2020 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 { IconComponent } from '@backstage/core-plugin-api';
+import MuiApartmentIcon from '@material-ui/icons/Apartment';
+import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage';
+import MuiCategoryIcon from '@material-ui/icons/Category';
+import MuiChatIcon from '@material-ui/icons/Chat';
+import MuiDashboardIcon from '@material-ui/icons/Dashboard';
+import MuiDocsIcon from '@material-ui/icons/Description';
+import MuiEmailIcon from '@material-ui/icons/Email';
+import MuiExtensionIcon from '@material-ui/icons/Extension';
+import MuiGitHubIcon from '@material-ui/icons/GitHub';
+import MuiHelpIcon from '@material-ui/icons/Help';
+import MuiLocationOnIcon from '@material-ui/icons/LocationOn';
+import MuiMemoryIcon from '@material-ui/icons/Memory';
+import MuiMenuBookIcon from '@material-ui/icons/MenuBook';
+import MuiPeopleIcon from '@material-ui/icons/People';
+import MuiPersonIcon from '@material-ui/icons/Person';
+import MuiWarningIcon from '@material-ui/icons/Warning';
+
+/** @public */
+export type AppIcons = {
+ 'kind:api': IconComponent;
+ 'kind:component': IconComponent;
+ 'kind:domain': IconComponent;
+ 'kind:group': IconComponent;
+ 'kind:location': IconComponent;
+ 'kind:system': IconComponent;
+ 'kind:user': IconComponent;
+
+ brokenImage: IconComponent;
+ catalog: IconComponent;
+ chat: IconComponent;
+ dashboard: IconComponent;
+ docs: IconComponent;
+ email: IconComponent;
+ github: IconComponent;
+ group: IconComponent;
+ help: IconComponent;
+ user: IconComponent;
+ warning: IconComponent;
+};
+
+export const defaultAppIcons: AppIcons = {
+ brokenImage: MuiBrokenImageIcon,
+ // To be confirmed: see https://github.com/backstage/backstage/issues/4970
+ catalog: MuiMenuBookIcon,
+ chat: MuiChatIcon,
+ dashboard: MuiDashboardIcon,
+ docs: MuiDocsIcon,
+ email: MuiEmailIcon,
+ github: MuiGitHubIcon,
+ group: MuiPeopleIcon,
+ help: MuiHelpIcon,
+ 'kind:api': MuiExtensionIcon,
+ 'kind:component': MuiMemoryIcon,
+ 'kind:domain': MuiApartmentIcon,
+ 'kind:group': MuiPeopleIcon,
+ 'kind:location': MuiLocationOnIcon,
+ 'kind:system': MuiCategoryIcon,
+ 'kind:user': MuiPersonIcon,
+ user: MuiPersonIcon,
+ warning: MuiWarningIcon,
+};
diff --git a/packages/app-defaults/src/createApp/index.ts b/packages/app-defaults/src/createApp/index.ts
new file mode 100644
index 0000000000..2a16a90bfa
--- /dev/null
+++ b/packages/app-defaults/src/createApp/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 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 { createApp, defaultConfigLoader } from './createApp';
+export type { AppIcons } from './icons';
+export * from './types';
diff --git a/packages/app-defaults/src/createApp/types.ts b/packages/app-defaults/src/createApp/types.ts
new file mode 100644
index 0000000000..78ef277c90
--- /dev/null
+++ b/packages/app-defaults/src/createApp/types.ts
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2020 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 { ComponentType } from 'react';
+import {
+ AnyApiFactory,
+ AppTheme,
+ ProfileInfo,
+ IconComponent,
+ BackstagePlugin,
+ RouteRef,
+ SubRouteRef,
+ ExternalRouteRef,
+ PluginOutput,
+} from '@backstage/core-plugin-api';
+import { AppConfig } from '@backstage/config';
+import { AppIcons } from './icons';
+
+/**
+ * Props for the `BootErrorPage` component of {@link AppComponents}.
+ *
+ * @public
+ */
+export type BootErrorPageProps = {
+ step: 'load-config' | 'load-chunk';
+ error: Error;
+};
+
+/**
+ * The outcome of signing in on the sign-in page.
+ *
+ * @public
+ */
+export type SignInResult = {
+ /**
+ * User ID that will be returned by the IdentityApi
+ */
+ userId: string;
+
+ profile: ProfileInfo;
+
+ /**
+ * Function used to retrieve an ID token for the signed in user.
+ */
+ getIdToken?: () => Promise;
+
+ /**
+ * Sign out handler that will be called if the user requests to sign out.
+ */
+ signOut?: () => Promise;
+};
+
+/**
+ * Props for the `SignInPage` component of {@link AppComponents}.
+ *
+ * @public
+ */
+export type SignInPageProps = {
+ /**
+ * Set the sign-in result for the app. This should only be called once.
+ */
+ onResult(result: SignInResult): void;
+};
+
+/**
+ * Props for the fallback error boundary.
+ *
+ * @public
+ */
+export type ErrorBoundaryFallbackProps = {
+ plugin?: BackstagePlugin;
+ error: Error;
+ resetError: () => void;
+};
+
+/**
+ * A set of replaceable core components that are part of every Backstage app.
+ *
+ * @public
+ */
+export type AppComponents = {
+ NotFoundErrorPage: ComponentType<{}>;
+ BootErrorPage: ComponentType;
+ Progress: ComponentType<{}>;
+ Router: ComponentType<{}>;
+ ErrorBoundaryFallback: ComponentType;
+ ThemeProvider: ComponentType<{}>;
+
+ /**
+ * An optional sign-in page that will be rendered instead of the AppRouter at startup.
+ *
+ * If a sign-in page is set, it will always be shown before the app, and it is up
+ * to the sign-in page to handle e.g. saving of login methods for subsequent visits.
+ *
+ * The sign-in page will be displayed until it has passed up a result to the parent,
+ * and which point the AppRouter and all of its children will be rendered instead.
+ */
+ SignInPage?: ComponentType;
+};
+
+/**
+ * A function that loads in the App config that will be accessible via the ConfigApi.
+ *
+ * If multiple config objects are returned in the array, values in the earlier configs
+ * will override later ones.
+ *
+ * @public
+ */
+export type AppConfigLoader = () => Promise;
+
+/**
+ * Extracts a union of the keys in a map whose value extends the given type
+ */
+type KeysWithType = {
+ [key in keyof Obj]: Obj[key] extends Type ? key : never;
+}[keyof Obj];
+
+/**
+ * Takes a map Map required values and makes all keys matching Keys optional
+ */
+type PartialKeys<
+ Map extends { [name in string]: any },
+ Keys extends keyof Map,
+> = Partial> & Required>;
+
+/**
+ * Creates a map of target routes with matching parameters based on a map of external routes.
+ */
+type TargetRouteMap<
+ ExternalRoutes extends { [name: string]: ExternalRouteRef },
+> = {
+ [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef<
+ infer Params,
+ any
+ >
+ ? RouteRef | SubRouteRef
+ : never;
+};
+
+/**
+ * A function that can bind from external routes of a given plugin, to concrete
+ * routes of other plugins. See {@link createApp}.
+ *
+ * @public
+ */
+export type AppRouteBinder = <
+ ExternalRoutes extends { [name: string]: ExternalRouteRef },
+>(
+ externalRoutes: ExternalRoutes,
+ targetRoutes: PartialKeys<
+ TargetRouteMap,
+ KeysWithType>
+ >,
+) => void;
+
+/**
+ * Internal helper type that represents a plugin with any type of output.
+ *
+ * @public
+ * @remarks
+ *
+ * The `type: string` type is there to handle output from newer or older plugin
+ * API versions that might not be supported by this version of the app API, but
+ * we don't want to break at the type checking level. We only use this more
+ * permissive type for the `createApp` options, as we otherwise want to stick
+ * to using the type for the outputs that we know about in this version of the
+ * app api.
+ *
+ * TODO(freben): This should be marked internal but that's not supported by the api report generation tools yet
+ */
+export type BackstagePluginWithAnyOutput = Omit<
+ BackstagePlugin,
+ 'output'
+> & {
+ output(): (PluginOutput | { type: string })[];
+};
+
+/**
+ * The options accepted by {@link createApp}.
+ *
+ * @public
+ */
+export type AppOptions = {
+ /**
+ * A collection of ApiFactories to register in the application to either
+ * add add new ones, or override factories provided by default or by plugins.
+ */
+ apis?: Iterable;
+
+ /**
+ * Supply icons to override the default ones.
+ */
+ icons?: Partial & { [key in string]: IconComponent };
+
+ /**
+ * A list of all plugins to include in the app.
+ */
+ plugins?: BackstagePluginWithAnyOutput[];
+
+ /**
+ * Supply components to the app to override the default ones.
+ */
+ components?: Partial;
+
+ /**
+ * Themes provided as a part of the app. By default two themes are included, one
+ * light variant of the default backstage theme, and one dark.
+ *
+ * This is the default config:
+ *
+ * ```
+ * [{
+ * id: 'light',
+ * title: 'Light Theme',
+ * variant: 'light',
+ * icon: ,
+ * Provider: ({ children }) => (
+ *
+ * {children}
+ *
+ * ),
+ * }, {
+ * id: 'dark',
+ * title: 'Dark Theme',
+ * variant: 'dark',
+ * icon: ,
+ * Provider: ({ children }) => (
+ *
+ * {children}
+ *
+ * ),
+ * }]
+ * ```
+ */
+ themes?: (Partial & Omit)[];
+
+ /**
+ * A function that loads in App configuration that will be accessible via
+ * the ConfigApi.
+ *
+ * Defaults to an empty config.
+ *
+ * TODO(Rugvip): Omitting this should instead default to loading in configuration
+ * that was packaged by the backstage-cli and default docker container boot script.
+ */
+ configLoader?: AppConfigLoader;
+
+ /**
+ * A function that is used to register associations between cross-plugin route
+ * references, enabling plugins to navigate between each other.
+ *
+ * The `bind` function that is passed in should be used to bind all external
+ * routes of all used plugins.
+ *
+ * ```ts
+ * bindRoutes({ bind }) {
+ * bind(docsPlugin.externalRoutes, {
+ * homePage: managePlugin.routes.managePage,
+ * })
+ * bind(homePagePlugin.externalRoutes, {
+ * settingsPage: settingsPlugin.routes.settingsPage,
+ * })
+ * }
+ * ```
+ */
+ bindRoutes?(context: { bind: AppRouteBinder }): void;
+};
+
+/**
+ * The public API of the output of {@link createApp}.
+ *
+ * @public
+ */
+export type BackstageApp = {
+ /**
+ * Returns all plugins registered for the app.
+ */
+ getPlugins(): BackstagePlugin[];
+
+ /**
+ * Get a common or custom icon for this app.
+ */
+ getSystemIcon(key: string): IconComponent | undefined;
+
+ /**
+ * Provider component that should wrap the Router created with getRouter()
+ * and any other components that need to be within the app context.
+ */
+ getProvider(): ComponentType<{}>;
+
+ /**
+ * Router component that should wrap the App Routes create with getRoutes()
+ * and any other components that should only be available while signed in.
+ */
+ getRouter(): ComponentType<{}>;
+};
+
+/**
+ * The central context providing runtime app specific state that plugin views
+ * want to consume.
+ *
+ * @public
+ */
+export type AppContext = {
+ /**
+ * Get a list of all plugins that are installed in the app.
+ */
+ getPlugins(): BackstagePlugin[];
+
+ /**
+ * Get a common or custom icon for this app.
+ */
+ getSystemIcon(key: string): IconComponent | undefined;
+
+ /**
+ * Get the components registered for various purposes in the app.
+ */
+ getComponents(): AppComponents;
+};
diff --git a/packages/core-components/src/appDefaults/withDefaults.tsx b/packages/app-defaults/src/createApp/withDefaults.tsx
similarity index 100%
rename from packages/core-components/src/appDefaults/withDefaults.tsx
rename to packages/app-defaults/src/createApp/withDefaults.tsx
diff --git a/packages/app-defaults/src/index.ts b/packages/app-defaults/src/index.ts
new file mode 100644
index 0000000000..62ef4d9077
--- /dev/null
+++ b/packages/app-defaults/src/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2020 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.
+ */
+
+/**
+ * Provides the default wiring of a Backstage App
+ *
+ * @packageDocumentation
+ */
+
+export * from './createApp';
diff --git a/packages/core-components/src/appDefaults/index.ts b/packages/app-defaults/src/setupTests.ts
similarity index 79%
rename from packages/core-components/src/appDefaults/index.ts
rename to packages/app-defaults/src/setupTests.ts
index 6ccc6a6257..c1d649f2ad 100644
--- a/packages/core-components/src/appDefaults/index.ts
+++ b/packages/app-defaults/src/setupTests.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Backstage Authors
+ * Copyright 2020 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.
@@ -14,5 +14,5 @@
* limitations under the License.
*/
-export { withDefaults } from './withDefaults';
-export type { OptionalAppOptions } from './withDefaults';
+import '@testing-library/jest-dom';
+import 'cross-fetch/polyfill';
diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts
index ea1fea9d5c..3c5e708360 100644
--- a/packages/core-components/src/index.ts
+++ b/packages/core-components/src/index.ts
@@ -25,4 +25,3 @@ export * from './hooks';
export * from './icons';
export * from './layout';
export * from './overridableComponents';
-export * from './appDefaults';