diff --git a/packages/app-next/.eslintrc.js b/packages/app-next/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/packages/app-next/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/packages/app-next/README.md b/packages/app-next/README.md
new file mode 100644
index 0000000000..21645609a9
--- /dev/null
+++ b/packages/app-next/README.md
@@ -0,0 +1,3 @@
+# example-app-next
+
+This package is an example of a Backstage application.
diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml
new file mode 100644
index 0000000000..463113fd55
--- /dev/null
+++ b/packages/app-next/app-config.yaml
@@ -0,0 +1,108 @@
+app:
+ packages: 'all' # ✨
+ extensions:
+ - core.router
+ - graphiql.page:
+ config:
+ path: /
+
+ # - core.signInPage:
+ # props:
+ # provider:
+ # id: google
+ # title: Google
+ # message: Sign In using Google
+ # apiRef: googleAuthApiRef # ???
+
+ # - core.nav:
+ # config:
+ # logo: assets/logo.png
+ # layout:
+ # - label: Search
+ # icon: search
+ # to: /search
+ # items:
+ # - point: search
+ # - type: divider
+ # - label: Menu
+ # icon: menu
+ # items:
+ # - label: Home
+ # icon: home
+ # to: /catalog
+ # - label: Create...
+ # icon: create
+ # to: /create
+ # - type: divider
+ # - type: scroll-wrapper
+ # items:
+ # - label: Tech Radar
+ # icon: map
+ # to: /tech-radar
+ # - label: GraphiQL
+ # icon: graphiql
+ # to: /graphiql
+ # - type: divider
+ # - point: shortcuts
+ # - type: space
+ # - type: divider
+ # - label: Settings
+ # icon: avatar
+ # to: /settings
+ # items:
+ # - point: settings
+ # - core.nav/search: '@backstage/plugin-search#SidebarSearchModal'
+ # - core.nav/shortcuts:
+ # use: '@backstage/plugin-shortcuts#Shortcuts'
+ # config:
+ # allowExternalLinks: true
+ # - core.nav/settings: '@backstage/plugin-user-settings#SidebarSettings'
+
+ # - core.pages.index:
+ # at:
+ # point: core.routes
+ # config:
+ # path: /
+ # use: 'react-router-dom#Navigate'
+ # config:
+ # to: /catalog
+
+ # - scaffolder.page:
+ # config:
+ # groups:
+ # - title: Recommended
+ # filter:
+ # entity.metadata.tags: recommended
+ # - scaffolder.page/fields: '@backstage/plugin-scaffolder#LowerCaseValuePickerFieldExtension'
+ # - scaffolder.page/layout: '@backstage/plugin-scaffolder#TwoColumnLayout'
+
+ # - search.page/content: 'app#customSearchPage' # custom search page from somewhere
+
+ # - user-settings.page.routes.advanced:
+ # at:
+ # point: user-settings.page/routes
+ # config:
+ # title: Advanced
+ # path: /advanced
+ # use: '@backstage/plugin-user-settings#AdvancedSettings'
+
+ # - entity.card.orphanWarning
+ # - entity.card.processingErrors
+ # - entity.card.about
+ # - entity.card.catalogGraph
+ # - entity.card.pagerDuty
+ # - entity.card.links
+ # - entity.card.labels
+ # - entity.card.githubInsightsLanguages
+ # - entity.card.githubInsightsReleases
+ # - entity.card.githubInsightsReadme:
+ # config:
+ # maxHeight: 350 # Throwing this config in to have an example, but in practice rely on default
+ # - entity.card.subcomponentsCard
+ # - entity.card.userProfile
+ # - entity.card.ownership
+ # - entity.card.likeDislikeRatings
+ # - entity.content.dependsOnComponents
+ # - entity.content.codeInsights
+ # - entity.content.todo
+ # - entity.content.feedbackResponse
diff --git a/packages/app-next/architecture.drawio.svg b/packages/app-next/architecture.drawio.svg
new file mode 100644
index 0000000000..b5ed4de71a
--- /dev/null
+++ b/packages/app-next/architecture.drawio.svg
@@ -0,0 +1,614 @@
+
diff --git a/packages/app-next/package.json b/packages/app-next/package.json
new file mode 100644
index 0000000000..d2642c2fde
--- /dev/null
+++ b/packages/app-next/package.json
@@ -0,0 +1,143 @@
+{
+ "name": "example-app-next",
+ "version": "0.0.0",
+ "private": true,
+ "backstage": {
+ "role": "frontend"
+ },
+ "bundled": true,
+ "dependencies": {
+ "@backstage/app-defaults": "workspace:^",
+ "@backstage/catalog-model": "workspace:^",
+ "@backstage/cli": "workspace:^",
+ "@backstage/config": "workspace:^",
+ "@backstage/core-app-api": "workspace:^",
+ "@backstage/core-components": "workspace:^",
+ "@backstage/core-plugin-api": "workspace:^",
+ "@backstage/frontend-app-api": "workspace:^",
+ "@backstage/frontend-plugin-api": "workspace:^",
+ "@backstage/integration-react": "workspace:^",
+ "@backstage/plugin-adr": "workspace:^",
+ "@backstage/plugin-airbrake": "workspace:^",
+ "@backstage/plugin-apache-airflow": "workspace:^",
+ "@backstage/plugin-api-docs": "workspace:^",
+ "@backstage/plugin-azure-devops": "workspace:^",
+ "@backstage/plugin-azure-sites": "workspace:^",
+ "@backstage/plugin-badges": "workspace:^",
+ "@backstage/plugin-catalog-common": "workspace:^",
+ "@backstage/plugin-catalog-graph": "workspace:^",
+ "@backstage/plugin-catalog-import": "workspace:^",
+ "@backstage/plugin-catalog-react": "workspace:^",
+ "@backstage/plugin-catalog-unprocessed-entities": "workspace:^",
+ "@backstage/plugin-circleci": "workspace:^",
+ "@backstage/plugin-cloudbuild": "workspace:^",
+ "@backstage/plugin-code-coverage": "workspace:^",
+ "@backstage/plugin-cost-insights": "workspace:^",
+ "@backstage/plugin-devtools": "workspace:^",
+ "@backstage/plugin-dynatrace": "workspace:^",
+ "@backstage/plugin-entity-feedback": "workspace:^",
+ "@backstage/plugin-explore": "workspace:^",
+ "@backstage/plugin-gcalendar": "workspace:^",
+ "@backstage/plugin-gcp-projects": "workspace:^",
+ "@backstage/plugin-github-actions": "workspace:^",
+ "@backstage/plugin-gocd": "workspace:^",
+ "@backstage/plugin-graphiql": "workspace:^",
+ "@backstage/plugin-home": "workspace:^",
+ "@backstage/plugin-jenkins": "workspace:^",
+ "@backstage/plugin-kafka": "workspace:^",
+ "@backstage/plugin-kubernetes": "workspace:^",
+ "@backstage/plugin-lighthouse": "workspace:^",
+ "@backstage/plugin-linguist": "workspace:^",
+ "@backstage/plugin-linguist-common": "workspace:^",
+ "@backstage/plugin-microsoft-calendar": "workspace:^",
+ "@backstage/plugin-newrelic": "workspace:^",
+ "@backstage/plugin-newrelic-dashboard": "workspace:^",
+ "@backstage/plugin-octopus-deploy": "workspace:^",
+ "@backstage/plugin-org": "workspace:^",
+ "@backstage/plugin-pagerduty": "workspace:^",
+ "@backstage/plugin-permission-react": "workspace:^",
+ "@backstage/plugin-playlist": "workspace:^",
+ "@backstage/plugin-puppetdb": "workspace:^",
+ "@backstage/plugin-rollbar": "workspace:^",
+ "@backstage/plugin-scaffolder": "workspace:^",
+ "@backstage/plugin-scaffolder-react": "workspace:^",
+ "@backstage/plugin-search": "workspace:^",
+ "@backstage/plugin-search-common": "workspace:^",
+ "@backstage/plugin-search-react": "workspace:^",
+ "@backstage/plugin-sentry": "workspace:^",
+ "@backstage/plugin-shortcuts": "workspace:^",
+ "@backstage/plugin-stack-overflow": "workspace:^",
+ "@backstage/plugin-stackstorm": "workspace:^",
+ "@backstage/plugin-tech-insights": "workspace:^",
+ "@backstage/plugin-tech-radar": "workspace:^",
+ "@backstage/plugin-techdocs": "workspace:^",
+ "@backstage/plugin-techdocs-module-addons-contrib": "workspace:^",
+ "@backstage/plugin-techdocs-react": "workspace:^",
+ "@backstage/plugin-todo": "workspace:^",
+ "@backstage/plugin-user-settings": "workspace:^",
+ "@backstage/theme": "workspace:^",
+ "@internal/plugin-catalog-customized": "workspace:^",
+ "@material-ui/core": "^4.12.2",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.61",
+ "@octokit/rest": "^19.0.3",
+ "@oriflame/backstage-plugin-score-card": "^0.7.0",
+ "@roadiehq/backstage-plugin-buildkite": "^2.0.8",
+ "@roadiehq/backstage-plugin-github-insights": "^2.0.5",
+ "@roadiehq/backstage-plugin-github-pull-requests": "^2.2.7",
+ "@roadiehq/backstage-plugin-travis-ci": "^2.0.5",
+ "history": "^5.0.0",
+ "lodash": "^4.17.21",
+ "prop-types": "^15.7.2",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2",
+ "react-router": "^6.3.0",
+ "react-router-dom": "^6.3.0",
+ "react-use": "^17.2.4",
+ "zen-observable": "^0.10.0"
+ },
+ "devDependencies": {
+ "@backstage/test-utils": "workspace:^",
+ "@testing-library/cypress": "^9.0.0",
+ "@testing-library/dom": "^8.0.0",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^12.1.3",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/jquery": "^3.3.34",
+ "@types/node": "^16.11.26",
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "@types/zen-observable": "^0.8.0",
+ "cross-env": "^7.0.0",
+ "cypress": "^10.0.0",
+ "eslint-plugin-cypress": "^2.10.3",
+ "start-server-and-test": "^1.10.11"
+ },
+ "scripts": {
+ "start": "backstage-cli package start --config ../../app-config.yaml --config app-config.yaml",
+ "build": "backstage-cli package build",
+ "clean": "backstage-cli package clean",
+ "test": "backstage-cli package test",
+ "lint": "backstage-cli package lint",
+ "test:e2e": "start-server-and-test start http://localhost:3000 cy:dev",
+ "test:e2e:ci": "start-server-and-test start http://localhost:3000 cy:run",
+ "cy:dev": "cypress open",
+ "cy:run": "cypress run"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "files": [
+ "dist"
+ ],
+ "license": "Apache-2.0"
+}
diff --git a/packages/app-next/public/android-chrome-192x192.png b/packages/app-next/public/android-chrome-192x192.png
new file mode 100644
index 0000000000..eec0ae25b9
Binary files /dev/null and b/packages/app-next/public/android-chrome-192x192.png differ
diff --git a/packages/app-next/public/android-chrome-512x512.png b/packages/app-next/public/android-chrome-512x512.png
new file mode 100644
index 0000000000..ac0578133b
Binary files /dev/null and b/packages/app-next/public/android-chrome-512x512.png differ
diff --git a/packages/app-next/public/apple-touch-icon.png b/packages/app-next/public/apple-touch-icon.png
new file mode 100644
index 0000000000..6a45afb38f
Binary files /dev/null and b/packages/app-next/public/apple-touch-icon.png differ
diff --git a/packages/app-next/public/favicon-16x16.png b/packages/app-next/public/favicon-16x16.png
new file mode 100644
index 0000000000..58cf61a35e
Binary files /dev/null and b/packages/app-next/public/favicon-16x16.png differ
diff --git a/packages/app-next/public/favicon-32x32.png b/packages/app-next/public/favicon-32x32.png
new file mode 100644
index 0000000000..c0915ece75
Binary files /dev/null and b/packages/app-next/public/favicon-32x32.png differ
diff --git a/packages/app-next/public/favicon.ico b/packages/app-next/public/favicon.ico
new file mode 100644
index 0000000000..5e45e5dfbd
Binary files /dev/null and b/packages/app-next/public/favicon.ico differ
diff --git a/packages/app-next/public/favicon.svg b/packages/app-next/public/favicon.svg
new file mode 100644
index 0000000000..7f2d375cd8
--- /dev/null
+++ b/packages/app-next/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/app-next/public/index.html b/packages/app-next/public/index.html
new file mode 100644
index 0000000000..0710a0d69d
--- /dev/null
+++ b/packages/app-next/public/index.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= config.getString('app.title') %>
+
+ <% if (config.has('app.datadogRum')) { %>
+
+ <% } %>
+
+
+
+
+
+
+
+
diff --git a/packages/app-next/public/manifest.json b/packages/app-next/public/manifest.json
new file mode 100644
index 0000000000..4a7c1b4ec4
--- /dev/null
+++ b/packages/app-next/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "Backstage",
+ "name": "Backstage",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "48x48",
+ "type": "image/png"
+ }
+ ],
+ "start_url": "./index.html",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/packages/app-next/public/robots.txt b/packages/app-next/public/robots.txt
new file mode 100644
index 0000000000..01b0f9a107
--- /dev/null
+++ b/packages/app-next/public/robots.txt
@@ -0,0 +1,2 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
diff --git a/packages/app-next/public/safari-pinned-tab.svg b/packages/app-next/public/safari-pinned-tab.svg
new file mode 100644
index 0000000000..0f500b3002
--- /dev/null
+++ b/packages/app-next/public/safari-pinned-tab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/app-next/src/App.test.tsx b/packages/app-next/src/App.test.tsx
new file mode 100644
index 0000000000..047c9821b5
--- /dev/null
+++ b/packages/app-next/src/App.test.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 React from 'react';
+import { renderWithEffects } from '@backstage/test-utils';
+
+describe('App', () => {
+ it('should render', async () => {
+ process.env = {
+ NODE_ENV: 'test',
+ APP_CONFIG: [
+ {
+ data: {
+ app: {
+ title: 'Test',
+ support: { url: 'http://localhost:7007/support' },
+ },
+ backend: { baseUrl: 'http://localhost:7007' },
+ lighthouse: {
+ baseUrl: 'http://localhost:3003',
+ },
+ techdocs: {
+ storageUrl: 'http://localhost:7007/api/techdocs/static/docs',
+ },
+ },
+ context: 'test',
+ },
+ ] as any,
+ };
+
+ const { default: App } = await import('./App');
+ const rendered = await renderWithEffects();
+ expect(rendered.baseElement).toBeInTheDocument();
+ });
+});
diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx
new file mode 100644
index 0000000000..b875fb3927
--- /dev/null
+++ b/packages/app-next/src/App.tsx
@@ -0,0 +1,113 @@
+/*
+ * 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 { graphiqlPlugin as legacyGraphiqlPlugin } from '@backstage/plugin-graphiql';
+import { createApp as createLegacyApp } from '@backstage/app-defaults';
+import { createApp } from '@backstage/frontend-app-api';
+import { graphiqlPlugin } from './examples/graphiqlPlugin';
+
+/*
+
+# Notes
+
+TODO:
+ - proper createApp
+ - connect extensions and plugins, provide method?
+ - higher level API for creating standard extensions + higher order framework API for creating those?
+ - extension config schema + validation
+ - figure out how to resolve configured extension ref to runtime value, e.g. '@backstage/plugin-graphiql#GraphiqlPage'
+ - make sure all shorthands work + tests
+ - figure out package structure / how to ship, frontend-plugin-api/frontend-app-api
+ - figure out routing, useRouteRef in the new system
+ - Legacy plugins / interop
+ - dynamic updates, runtime API
+
+*/
+
+/* core */
+
+// const discoverPackages = async () => {
+// // stub for now, deferring package discovery til later
+// return ['@backstage/plugin-graphiql'];
+// };
+
+/* graphiql package */
+
+/* app.tsx */
+
+const app = createApp({
+ plugins: [graphiqlPlugin],
+ // bindRoutes({ bind }) {
+ // bind(catalogPlugin.externalRoutes, {
+ // createComponent: scaffolderPlugin.routes.root,
+ // });
+ // bind(scaffolderPlugin.externalRoutes, {
+ // registerComponent: catalogImportPlugin.routes.importPage,
+ // });
+ // },
+});
+
+const legacyApp = createLegacyApp({ plugins: [legacyGraphiqlPlugin] });
+
+export default legacyApp.createRoot(app.createRoot());
+
+// const routes = (
+//
+// {/* } />
+// } />
+// }
+// >
+//
+//
+//
+//
+//
+//
+
+//
+//
+//
+//
+//
+
+//
+//
+//
+//
+//
+//
+// }
+// /> */}
+// {/* }
+// /> */}
+// } />
+//
+// );
+
+// export default app.createRoot(
+// <>
+// {/*
+// */}
+// {routes}
+// >,
+// );
diff --git a/packages/app-next/src/examples/graphiqlPlugin.tsx b/packages/app-next/src/examples/graphiqlPlugin.tsx
new file mode 100644
index 0000000000..87f2bb242c
--- /dev/null
+++ b/packages/app-next/src/examples/graphiqlPlugin.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 {
+ createExtension,
+ createPlugin,
+ coreExtensionData,
+} from '@backstage/frontend-plugin-api';
+import { Router as GraphiQLPage } from '@backstage/plugin-graphiql';
+
+export const GraphiqlPageExtension = createExtension({
+ output: {
+ component: coreExtensionData.reactComponent,
+ path: coreExtensionData.routePath,
+ },
+ factory({ bind, config }) {
+ bind.component(() => {
+ return ;
+ });
+ // TODO: In need of schemas and type safety
+ bind.path((config as { path: string }).path);
+ },
+});
+
+export const graphiqlPlugin = createPlugin({
+ id: 'graphiql',
+ defaultExtensionInstances: [
+ {
+ id: 'graphiql.page',
+ at: 'core.router/routes',
+ extension: GraphiqlPageExtension,
+ config: { path: '/graphiql' },
+ },
+ ],
+});
diff --git a/packages/app-next/src/index.tsx b/packages/app-next/src/index.tsx
new file mode 100644
index 0000000000..b15bc4c102
--- /dev/null
+++ b/packages/app-next/src/index.tsx
@@ -0,0 +1,22 @@
+/*
+ * 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 '@backstage/cli/asset-types';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+
+ReactDOM.render(, document.getElementById('root'));
diff --git a/packages/app-next/src/setupTests.ts b/packages/app-next/src/setupTests.ts
new file mode 100644
index 0000000000..23fcbe9676
--- /dev/null
+++ b/packages/app-next/src/setupTests.ts
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';
diff --git a/packages/frontend-app-api/.eslintrc.js b/packages/frontend-app-api/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/packages/frontend-app-api/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/packages/frontend-app-api/README.md b/packages/frontend-app-api/README.md
new file mode 100644
index 0000000000..5cc5073f61
--- /dev/null
+++ b/packages/frontend-app-api/README.md
@@ -0,0 +1,12 @@
+# @backstage/frontend-app-api
+
+_This package was created through the Backstage CLI_.
+
+## Installation
+
+Install the package via Yarn:
+
+```sh
+cd # if within a monorepo
+yarn add @backstage/frontend-app-api
+```
diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md
new file mode 100644
index 0000000000..bf7db4451b
--- /dev/null
+++ b/packages/frontend-app-api/api-report.md
@@ -0,0 +1,14 @@
+## API Report File for "@backstage/frontend-app-api"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { BackstagePlugin } from '@backstage/frontend-plugin-api';
+
+// @public (undocumented)
+export function createApp(options: { plugins: BackstagePlugin[] }): {
+ createRoot(): JSX.Element;
+};
+```
diff --git a/packages/frontend-app-api/config.d.ts b/packages/frontend-app-api/config.d.ts
new file mode 100644
index 0000000000..81c24d1185
--- /dev/null
+++ b/packages/frontend-app-api/config.d.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 interface Config {
+ app?: {
+ /**
+ * @deepVisibility frontend
+ */
+ extensions?:
+ | string
+ | {
+ [extensionId: string]:
+ | boolean
+ | string
+ | {
+ at?: string;
+ extension?: string;
+ config?: unknown;
+ };
+ };
+ };
+}
diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json
new file mode 100644
index 0000000000..c085c6dba6
--- /dev/null
+++ b/packages/frontend-app-api/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@backstage/frontend-app-api",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "web-library"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^",
+ "@testing-library/jest-dom": "^5.10.1"
+ },
+ "configSchema": "config.d.ts",
+ "files": [
+ "config.d.ts",
+ "dist"
+ ],
+ "dependencies": {
+ "@backstage/config": "workspace:^",
+ "@backstage/frontend-plugin-api": "workspace:^",
+ "@backstage/plugin-graphiql": "workspace:^",
+ "lodash": "^4.17.21"
+ },
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.0",
+ "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
+ }
+}
diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx
new file mode 100644
index 0000000000..46cd0a7aed
--- /dev/null
+++ b/packages/frontend-app-api/src/createApp.tsx
@@ -0,0 +1,206 @@
+/*
+ * 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 { Config, ConfigReader } from '@backstage/config';
+import {
+ ExtensionInstanceConfig,
+ BackstagePlugin,
+ coreExtensionData,
+} from '@backstage/frontend-plugin-api';
+import { RouteExtension } from './extensions/RouteExtension';
+import {
+ createExtensionInstance,
+ ExtensionInstance,
+} from './createExtensionInstance';
+
+// Since we'll never merge arrays in config the config reader context
+// isn't too much of a help. Fall back to manual config reading logic
+// as the Config interface makes it quite hard for us otherwise.
+function readAppExtensionConfigs(
+ rootConfig: Config,
+): Partial[] {
+ const arr = rootConfig.getOptional('app.extensions');
+ if (!Array.isArray(arr)) {
+ if (arr === undefined) {
+ return [];
+ }
+ // This will throw, and show which part of config had the wrong type
+ rootConfig.getConfigArray('app.extensions');
+ return [];
+ }
+
+ return arr.map((value, index) => {
+ function errorMsg(msg: string, key?: string, prop?: string) {
+ return `Invalid extension configuration at app.extensions[${index}]${
+ key ? `[${key}]` : ''
+ }${prop ? `.${prop}` : ''}, ${msg}`;
+ }
+
+ if (typeof value === 'string') {
+ return { id: value };
+ } else if (
+ typeof value !== 'object' ||
+ value === null ||
+ Array.isArray(value)
+ ) {
+ throw new Error(errorMsg('must be a string or an object'));
+ }
+
+ const keys = Object.keys(value);
+ if (keys.length !== 1) {
+ const joinedKeys = `"${keys.join('", "')}"`;
+ throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`));
+ }
+
+ const key = keys[0];
+ const obj = value[key];
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
+ throw new Error(errorMsg('must be an object', key));
+ }
+ const at = obj.at;
+ if (at !== undefined && typeof at !== 'string') {
+ throw new Error(errorMsg('must be a string', key, 'at'));
+ }
+ const extension = obj.extension;
+ if (extension !== undefined && typeof extension !== 'string') {
+ throw new Error(errorMsg('must be a string', key, 'extension'));
+ }
+ if (extension) {
+ throw new Error('TODO: implement extension resolution');
+ }
+ return { id: key, at, config: obj.config /* validate later */ };
+ });
+}
+
+/** @public */
+export function createApp(options: { plugins: BackstagePlugin[] }): {
+ createRoot(): JSX.Element;
+} {
+ const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any);
+
+ // pull in default extension instance from discovered packages
+ // apply config to adjust default extension instances and add more
+ const extensionInstanceConfigs = [
+ ...options.plugins.flatMap(plugin => plugin.defaultExtensionInstances),
+ {
+ id: 'core.router',
+ at: 'root/default',
+ extension: RouteExtension,
+ config: undefined,
+ },
+ ];
+
+ const appExtensionConfigs = readAppExtensionConfigs(appConfig);
+ for (const appExtensionConfig of appExtensionConfigs) {
+ const existingConfig = extensionInstanceConfigs.find(
+ e => e.id === appExtensionConfig.id,
+ );
+ if (existingConfig) {
+ if (appExtensionConfig.at) {
+ existingConfig.at = appExtensionConfig.at;
+ }
+ if (appExtensionConfig.extension) {
+ // TODO: do we want to reset config here? it might be completely
+ // unrelated to the previous one
+ existingConfig.extension = appExtensionConfig.extension;
+ }
+ if (appExtensionConfig.config) {
+ // TODO: merge config?
+ existingConfig.config = appExtensionConfig.config;
+ }
+ } else if (appExtensionConfig.id) {
+ const { id, at, extension, config } = appExtensionConfig;
+ if (!at || !extension) {
+ throw new Error(`Extension ${appExtensionConfig.id} is incomplete`);
+ }
+ extensionInstanceConfigs.push({ id, at, extension, config });
+ }
+ }
+
+ // Create attachment map so that we can look attachments up during instance creation
+ const attachmentMap = new Map<
+ string,
+ Map
+ >();
+ for (const instanceConfig of extensionInstanceConfigs) {
+ const [extensionId, pointId = 'default'] = instanceConfig.at.split('/');
+
+ let pointMap = attachmentMap.get(extensionId);
+ if (!pointMap) {
+ pointMap = new Map();
+ attachmentMap.set(extensionId, pointMap);
+ }
+
+ let instances = pointMap.get(pointId);
+ if (!instances) {
+ instances = [];
+ pointMap.set(pointId, instances);
+ }
+
+ instances.push(instanceConfig);
+ }
+
+ const instances = new Map();
+
+ function createInstance(
+ instanceConfig: ExtensionInstanceConfig,
+ ): ExtensionInstance {
+ const existingInstance = instances.get(instanceConfig.id);
+ if (existingInstance) {
+ return existingInstance;
+ }
+
+ const attachments = Object.fromEntries(
+ Array.from(attachmentMap.get(instanceConfig.id)?.entries() ?? []).map(
+ ([inputName, attachmentConfigs]) => [
+ inputName,
+ attachmentConfigs.map(createInstance),
+ ],
+ ),
+ );
+
+ return createExtensionInstance({
+ id: instanceConfig.id,
+ config: instanceConfig.config,
+ extension: instanceConfig.extension,
+ attachments,
+ });
+ }
+
+ const rootConfigs = attachmentMap.get('root')?.get('default') ?? [];
+ const rootInstances = rootConfigs.map(instanceConfig =>
+ createInstance(instanceConfig),
+ );
+
+ return {
+ createRoot() {
+ const rootComponents = rootInstances.map(
+ e =>
+ e.data.get(
+ coreExtensionData.reactComponent.id,
+ ) as typeof coreExtensionData.reactComponent.T,
+ );
+ return (
+ <>
+ {rootComponents.map(Component => (
+
+ ))}
+ >
+ );
+ },
+ };
+}
diff --git a/packages/frontend-app-api/src/createExtensionInstance.ts b/packages/frontend-app-api/src/createExtensionInstance.ts
new file mode 100644
index 0000000000..0b62df74ae
--- /dev/null
+++ b/packages/frontend-app-api/src/createExtensionInstance.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 '@backstage/frontend-plugin-api';
+import mapValues from 'lodash/mapValues';
+
+/** @internal */
+export interface ExtensionInstance {
+ id: string;
+ data: Map;
+ $$type: 'extension-instance';
+}
+
+/** @internal */
+export function createExtensionInstance(options: {
+ id: string;
+ extension: Extension;
+ config: unknown;
+ attachments: Record;
+}): ExtensionInstance {
+ const { extension, config, attachments } = options;
+ const extensionData = new Map();
+ extension.factory({
+ config,
+ bind: mapValues(extension.output, ref => {
+ return (value: unknown) => extensionData.set(ref.id, value);
+ }),
+ inputs: mapValues(
+ extension.inputs,
+ ({ extensionData: pointData }, inputName) => {
+ // TODO: validation
+ return (attachments[inputName] ?? []).map(attachment =>
+ mapValues(pointData, ref => attachment.data.get(ref.id)),
+ );
+ },
+ ),
+ });
+ return { id: options.id, data: extensionData, $$type: 'extension-instance' };
+}
diff --git a/packages/frontend-app-api/src/extensions/RouteExtension.tsx b/packages/frontend-app-api/src/extensions/RouteExtension.tsx
new file mode 100644
index 0000000000..ef7e34ea30
--- /dev/null
+++ b/packages/frontend-app-api/src/extensions/RouteExtension.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 {
+ createExtension,
+ coreExtensionData,
+} from '@backstage/frontend-plugin-api';
+import { BrowserRouter, useRoutes } from 'react-router-dom';
+
+export const RouteExtension = createExtension({
+ inputs: {
+ routes: {
+ extensionData: {
+ path: coreExtensionData.routePath,
+ component: coreExtensionData.reactComponent,
+ },
+ },
+ },
+ output: {
+ component: coreExtensionData.reactComponent,
+ },
+ factory({ bind, inputs }) {
+ const Routes = () => {
+ const element = useRoutes(
+ inputs.routes.map(route => ({
+ path: route.path,
+ element: ,
+ })),
+ );
+
+ return element;
+ };
+ bind.component(() => (
+
+
+
+ ));
+ },
+});
diff --git a/packages/frontend-app-api/src/index.ts b/packages/frontend-app-api/src/index.ts
new file mode 100644
index 0000000000..45fe9413ba
--- /dev/null
+++ b/packages/frontend-app-api/src/index.ts
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * TODO
+ *
+ * @packageDocumentation
+ */
+
+export { createApp } from './createApp';
diff --git a/packages/frontend-app-api/src/setupTests.ts b/packages/frontend-app-api/src/setupTests.ts
new file mode 100644
index 0000000000..c30f1d15cb
--- /dev/null
+++ b/packages/frontend-app-api/src/setupTests.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.
+ */
+
+import '@testing-library/jest-dom';
diff --git a/packages/frontend-plugin-api/.eslintrc.js b/packages/frontend-plugin-api/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/packages/frontend-plugin-api/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/packages/frontend-plugin-api/README.md b/packages/frontend-plugin-api/README.md
new file mode 100644
index 0000000000..43cb4082dc
--- /dev/null
+++ b/packages/frontend-plugin-api/README.md
@@ -0,0 +1,12 @@
+# @backstage/frontend-plugin-api
+
+_This package was created through the Backstage CLI_.
+
+## Installation
+
+Install the package via Yarn:
+
+```sh
+cd # if within a monorepo
+yarn add @backstage/frontend-plugin-api
+```
diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md
new file mode 100644
index 0000000000..4337b0b8c2
--- /dev/null
+++ b/packages/frontend-plugin-api/api-report.md
@@ -0,0 +1,124 @@
+## API Report File for "@backstage/frontend-plugin-api"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { ComponentType } from 'react';
+
+// @public (undocumented)
+export type AnyExtensionDataMap = Record>;
+
+// @public (undocumented)
+export interface BackstagePlugin {
+ // (undocumented)
+ $$type: 'backstage-plugin';
+ // (undocumented)
+ defaultExtensionInstances: ExtensionInstanceConfig[];
+ // (undocumented)
+ id: string;
+}
+
+// @public (undocumented)
+export interface BackstagePluginOptions {
+ // (undocumented)
+ defaultExtensionInstances?: ExtensionInstanceConfig[];
+ // (undocumented)
+ id: string;
+}
+
+// @public (undocumented)
+export const coreExtensionData: {
+ reactComponent: ExtensionDataRef>;
+ routePath: ExtensionDataRef;
+};
+
+// @public (undocumented)
+export function createExtension<
+ TData extends AnyExtensionDataMap,
+ TPoint extends Record<
+ string,
+ {
+ extensionData: AnyExtensionDataMap;
+ }
+ >,
+>(options: CreateExtensionOptions): Extension;
+
+// @public (undocumented)
+export interface CreateExtensionOptions<
+ TData extends AnyExtensionDataMap,
+ TPoint extends Record<
+ string,
+ {
+ extensionData: AnyExtensionDataMap;
+ }
+ >,
+> {
+ // (undocumented)
+ factory(options: {
+ bind: ExtensionDataBind;
+ config?: unknown;
+ inputs: {
+ [pointName in keyof TPoint]: ExtensionDataValue<
+ TPoint[pointName]['extensionData']
+ >[];
+ };
+ }): void;
+ // (undocumented)
+ inputs?: TPoint;
+ // (undocumented)
+ output: TData;
+}
+
+// @public (undocumented)
+export function createPlugin(options: BackstagePluginOptions): BackstagePlugin;
+
+// @public (undocumented)
+export interface Extension {
+ // (undocumented)
+ $$type: 'extension';
+ // (undocumented)
+ factory(options: {
+ bind: ExtensionDataBind;
+ config?: unknown;
+ inputs: Record>>;
+ }): void;
+ // (undocumented)
+ inputs: Record<
+ string,
+ {
+ extensionData: AnyExtensionDataMap;
+ }
+ >;
+ // (undocumented)
+ output: AnyExtensionDataMap;
+}
+
+// @public (undocumented)
+export type ExtensionDataBind = {
+ [K in keyof TData]: (value: TData[K]['T']) => void;
+};
+
+// @public (undocumented)
+export type ExtensionDataRef = {
+ id: string;
+ T: T;
+ $$type: 'extension-data';
+};
+
+// @public (undocumented)
+export type ExtensionDataValue = {
+ [K in keyof TData]: TData[K]['T'];
+};
+
+// @public (undocumented)
+export interface ExtensionInstanceConfig {
+ // (undocumented)
+ at: string;
+ // (undocumented)
+ config: unknown;
+ // (undocumented)
+ extension: Extension;
+ // (undocumented)
+ id: string;
+}
+```
diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json
new file mode 100644
index 0000000000..731e66fed0
--- /dev/null
+++ b/packages/frontend-plugin-api/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@backstage/frontend-plugin-api",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "web-library"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^",
+ "@testing-library/jest-dom": "^5.10.1"
+ },
+ "files": [
+ "dist"
+ ],
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.0"
+ },
+ "dependencies": {
+ "lodash": "^4.17.21"
+ }
+}
diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts
new file mode 100644
index 0000000000..e36391131d
--- /dev/null
+++ b/packages/frontend-plugin-api/src/index.ts
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+/**
+ * TODO
+ *
+ * @packageDocumentation
+ */
+
+export {
+ createExtension,
+ coreExtensionData,
+ createPlugin,
+ type ExtensionInstanceConfig,
+ type BackstagePlugin,
+ type Extension,
+ type AnyExtensionDataMap,
+ type BackstagePluginOptions,
+ type CreateExtensionOptions,
+ type ExtensionDataBind,
+ type ExtensionDataRef,
+ type ExtensionDataValue,
+} from './types';
diff --git a/packages/frontend-plugin-api/src/setupTests.ts b/packages/frontend-plugin-api/src/setupTests.ts
new file mode 100644
index 0000000000..c30f1d15cb
--- /dev/null
+++ b/packages/frontend-plugin-api/src/setupTests.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.
+ */
+
+import '@testing-library/jest-dom';
diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts
new file mode 100644
index 0000000000..b2d131c62f
--- /dev/null
+++ b/packages/frontend-plugin-api/src/types.ts
@@ -0,0 +1,116 @@
+/*
+ * 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 { ComponentType } from 'react';
+
+/** @public */
+export type ExtensionDataRef = {
+ id: string;
+ T: T;
+ $$type: 'extension-data';
+};
+
+/** @public */
+export function createExtensionDataRef(id: string): ExtensionDataRef {
+ return { id, $$type: 'extension-data' } as ExtensionDataRef;
+}
+
+/** @public */
+export const coreExtensionData = {
+ reactComponent: createExtensionDataRef('core.reactComponent'),
+ routePath: createExtensionDataRef('core.routing.path'),
+};
+
+/** @public */
+export type AnyExtensionDataMap = Record>;
+
+/** @public */
+export type ExtensionDataBind = {
+ [K in keyof TData]: (value: TData[K]['T']) => void;
+};
+
+/** @public */
+export type ExtensionDataValue = {
+ [K in keyof TData]: TData[K]['T'];
+};
+
+/** @public */
+export interface CreateExtensionOptions<
+ TData extends AnyExtensionDataMap,
+ TPoint extends Record,
+> {
+ inputs?: TPoint;
+ output: TData;
+ factory(options: {
+ bind: ExtensionDataBind;
+ config?: unknown;
+ inputs: {
+ [pointName in keyof TPoint]: ExtensionDataValue<
+ TPoint[pointName]['extensionData']
+ >[];
+ };
+ }): void;
+}
+
+/** @public */
+export interface Extension {
+ $$type: 'extension';
+ inputs: Record;
+ output: AnyExtensionDataMap;
+ factory(options: {
+ bind: ExtensionDataBind;
+ config?: unknown;
+ inputs: Record>>;
+ }): void;
+}
+
+/** @public */
+export function createExtension<
+ TData extends AnyExtensionDataMap,
+ TPoint extends Record,
+>(options: CreateExtensionOptions): Extension {
+ return { ...options, $$type: 'extension', inputs: options.inputs ?? {} };
+}
+
+/** @public */
+export interface ExtensionInstanceConfig {
+ id: string;
+ at: string;
+ extension: Extension;
+ config: unknown;
+}
+
+/** @public */
+export interface BackstagePluginOptions {
+ id: string;
+ defaultExtensionInstances?: ExtensionInstanceConfig[];
+}
+
+/** @public */
+export interface BackstagePlugin {
+ $$type: 'backstage-plugin';
+ id: string;
+ defaultExtensionInstances: ExtensionInstanceConfig[];
+}
+
+/** @public */
+export function createPlugin(options: BackstagePluginOptions): BackstagePlugin {
+ return {
+ ...options,
+ $$type: 'backstage-plugin',
+ defaultExtensionInstances: options.defaultExtensionInstances ?? [],
+ };
+}
diff --git a/yarn.lock b/yarn.lock
index 2e780973d7..05a48457c9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4132,6 +4132,34 @@ __metadata:
languageName: unknown
linkType: soft
+"@backstage/frontend-app-api@workspace:^, @backstage/frontend-app-api@workspace:packages/frontend-app-api":
+ version: 0.0.0-use.local
+ resolution: "@backstage/frontend-app-api@workspace:packages/frontend-app-api"
+ dependencies:
+ "@backstage/cli": "workspace:^"
+ "@backstage/config": "workspace:^"
+ "@backstage/frontend-plugin-api": "workspace:^"
+ "@backstage/plugin-graphiql": "workspace:^"
+ "@testing-library/jest-dom": ^5.10.1
+ lodash: ^4.17.21
+ peerDependencies:
+ react: ^16.13.1 || ^17.0.0
+ react-router-dom: 6.0.0-beta.0 || ^6.3.0
+ languageName: unknown
+ linkType: soft
+
+"@backstage/frontend-plugin-api@workspace:^, @backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api":
+ version: 0.0.0-use.local
+ resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api"
+ dependencies:
+ "@backstage/cli": "workspace:^"
+ "@testing-library/jest-dom": ^5.10.1
+ lodash: ^4.17.21
+ peerDependencies:
+ react: ^16.13.1 || ^17.0.0
+ languageName: unknown
+ linkType: soft
+
"@backstage/integration-aws-node@workspace:^, @backstage/integration-aws-node@workspace:packages/integration-aws-node":
version: 0.0.0-use.local
resolution: "@backstage/integration-aws-node@workspace:packages/integration-aws-node"
@@ -25316,6 +25344,116 @@ __metadata:
languageName: node
linkType: hard
+"example-app-next@workspace:packages/app-next":
+ version: 0.0.0-use.local
+ resolution: "example-app-next@workspace:packages/app-next"
+ dependencies:
+ "@backstage/app-defaults": "workspace:^"
+ "@backstage/catalog-model": "workspace:^"
+ "@backstage/cli": "workspace:^"
+ "@backstage/config": "workspace:^"
+ "@backstage/core-app-api": "workspace:^"
+ "@backstage/core-components": "workspace:^"
+ "@backstage/core-plugin-api": "workspace:^"
+ "@backstage/frontend-app-api": "workspace:^"
+ "@backstage/frontend-plugin-api": "workspace:^"
+ "@backstage/integration-react": "workspace:^"
+ "@backstage/plugin-adr": "workspace:^"
+ "@backstage/plugin-airbrake": "workspace:^"
+ "@backstage/plugin-apache-airflow": "workspace:^"
+ "@backstage/plugin-api-docs": "workspace:^"
+ "@backstage/plugin-azure-devops": "workspace:^"
+ "@backstage/plugin-azure-sites": "workspace:^"
+ "@backstage/plugin-badges": "workspace:^"
+ "@backstage/plugin-catalog-common": "workspace:^"
+ "@backstage/plugin-catalog-graph": "workspace:^"
+ "@backstage/plugin-catalog-import": "workspace:^"
+ "@backstage/plugin-catalog-react": "workspace:^"
+ "@backstage/plugin-catalog-unprocessed-entities": "workspace:^"
+ "@backstage/plugin-circleci": "workspace:^"
+ "@backstage/plugin-cloudbuild": "workspace:^"
+ "@backstage/plugin-code-coverage": "workspace:^"
+ "@backstage/plugin-cost-insights": "workspace:^"
+ "@backstage/plugin-devtools": "workspace:^"
+ "@backstage/plugin-dynatrace": "workspace:^"
+ "@backstage/plugin-entity-feedback": "workspace:^"
+ "@backstage/plugin-explore": "workspace:^"
+ "@backstage/plugin-gcalendar": "workspace:^"
+ "@backstage/plugin-gcp-projects": "workspace:^"
+ "@backstage/plugin-github-actions": "workspace:^"
+ "@backstage/plugin-gocd": "workspace:^"
+ "@backstage/plugin-graphiql": "workspace:^"
+ "@backstage/plugin-home": "workspace:^"
+ "@backstage/plugin-jenkins": "workspace:^"
+ "@backstage/plugin-kafka": "workspace:^"
+ "@backstage/plugin-kubernetes": "workspace:^"
+ "@backstage/plugin-lighthouse": "workspace:^"
+ "@backstage/plugin-linguist": "workspace:^"
+ "@backstage/plugin-linguist-common": "workspace:^"
+ "@backstage/plugin-microsoft-calendar": "workspace:^"
+ "@backstage/plugin-newrelic": "workspace:^"
+ "@backstage/plugin-newrelic-dashboard": "workspace:^"
+ "@backstage/plugin-octopus-deploy": "workspace:^"
+ "@backstage/plugin-org": "workspace:^"
+ "@backstage/plugin-pagerduty": "workspace:^"
+ "@backstage/plugin-permission-react": "workspace:^"
+ "@backstage/plugin-playlist": "workspace:^"
+ "@backstage/plugin-puppetdb": "workspace:^"
+ "@backstage/plugin-rollbar": "workspace:^"
+ "@backstage/plugin-scaffolder": "workspace:^"
+ "@backstage/plugin-scaffolder-react": "workspace:^"
+ "@backstage/plugin-search": "workspace:^"
+ "@backstage/plugin-search-common": "workspace:^"
+ "@backstage/plugin-search-react": "workspace:^"
+ "@backstage/plugin-sentry": "workspace:^"
+ "@backstage/plugin-shortcuts": "workspace:^"
+ "@backstage/plugin-stack-overflow": "workspace:^"
+ "@backstage/plugin-stackstorm": "workspace:^"
+ "@backstage/plugin-tech-insights": "workspace:^"
+ "@backstage/plugin-tech-radar": "workspace:^"
+ "@backstage/plugin-techdocs": "workspace:^"
+ "@backstage/plugin-techdocs-module-addons-contrib": "workspace:^"
+ "@backstage/plugin-techdocs-react": "workspace:^"
+ "@backstage/plugin-todo": "workspace:^"
+ "@backstage/plugin-user-settings": "workspace:^"
+ "@backstage/test-utils": "workspace:^"
+ "@backstage/theme": "workspace:^"
+ "@internal/plugin-catalog-customized": "workspace:^"
+ "@material-ui/core": ^4.12.2
+ "@material-ui/icons": ^4.9.1
+ "@material-ui/lab": 4.0.0-alpha.61
+ "@octokit/rest": ^19.0.3
+ "@oriflame/backstage-plugin-score-card": ^0.7.0
+ "@roadiehq/backstage-plugin-buildkite": ^2.0.8
+ "@roadiehq/backstage-plugin-github-insights": ^2.0.5
+ "@roadiehq/backstage-plugin-github-pull-requests": ^2.2.7
+ "@roadiehq/backstage-plugin-travis-ci": ^2.0.5
+ "@testing-library/cypress": ^9.0.0
+ "@testing-library/dom": ^8.0.0
+ "@testing-library/jest-dom": ^5.10.1
+ "@testing-library/react": ^12.1.3
+ "@testing-library/user-event": ^14.0.0
+ "@types/jquery": ^3.3.34
+ "@types/node": ^16.11.26
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ "@types/zen-observable": ^0.8.0
+ cross-env: ^7.0.0
+ cypress: ^10.0.0
+ eslint-plugin-cypress: ^2.10.3
+ history: ^5.0.0
+ lodash: ^4.17.21
+ prop-types: ^15.7.2
+ react: ^17.0.2
+ react-dom: ^17.0.2
+ react-router: ^6.3.0
+ react-router-dom: ^6.3.0
+ react-use: ^17.2.4
+ start-server-and-test: ^1.10.11
+ zen-observable: ^0.10.0
+ languageName: unknown
+ linkType: soft
+
"example-app@link:../app::locator=example-backend%40workspace%3Apackages%2Fbackend":
version: 0.0.0-use.local
resolution: "example-app@link:../app::locator=example-backend%40workspace%3Apackages%2Fbackend"