Merge branch 'backstage:master' into patch-1
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/config-loader': patch
|
||||
---
|
||||
|
||||
Correctly resolve config targets into absolute paths
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
---
|
||||
|
||||
Create an experimental plugin that is compatible with the declarative integration system, it is exported from the `/alpha` subpath.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-stack-overflow-collator': minor
|
||||
---
|
||||
|
||||
Extract a package for the Stack Overflow new backend system plugin.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Fix `RoutedTabs` so that it does not explode without tabs.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Switch from using deprecated `@esbuild-kit/*` packages to using `tsx`. This also switches to using the new module loader `register` API when available, avoiding the experimental warning when starting backends.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Migrate catalog entity cards to new frontend system extension format.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-node': patch
|
||||
---
|
||||
|
||||
Fix highlighting for non-string fields on the `Lunr` search engine implementation.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Use default extensions boundary and suspense on the alpha declarative `createCatalogFilterExtension` extension factory.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-module-addons-contrib': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Fixed navigation bug that caused users to not be scrolled to the top of a new page. Fixed navigation bug where using backwards and forwards browser navigation did not scroll users to the correct place on the TechDoc page.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-gitlab': patch
|
||||
---
|
||||
|
||||
Added try catch around fetching gitlab group users to prevent refresh from failing completely while only a select number of groups might not be able to load correctly.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Export alpha routes and nav item extension, only available for applications that uses the new Frontend system.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-stack-overflow-backend': patch
|
||||
---
|
||||
|
||||
Deprecate package in favor of the new `@backstage/plugin-search-backend-module-stack-overflow-collator` module.
|
||||
|
||||
The search collator `requestParams` option is optional now, so its default value is `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as defined in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Refactor internal extension instance system into an app graph.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
---
|
||||
|
||||
Add info about the entity when tech docs fail to build
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-code-coverage-backend': patch
|
||||
---
|
||||
|
||||
Added support for new backend system
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Add examples for `github:environment:create` scaffolder action & improve related tests
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Initial entity page implementation for new frontend system at `/alpha`, with an overview page enabled by default and the about card available as an optional card.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Added entity page content for the new plugin exported via `/alpha`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-stack-overflow': patch
|
||||
---
|
||||
|
||||
Migrate package to the new Frontend system, the new module is distributed with a `/alpha` subpath.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Added new APIs at the `/alpha` subpath for creating entity page cards and content for the new frontend system.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Updated `app.extensions` configuration schema.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-tech-insights-backend': patch
|
||||
---
|
||||
|
||||
Add `factRetrieverId` to the fact retriever's logger metadata.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-azure-devops': patch
|
||||
---
|
||||
|
||||
Consolidated getting the annotation values into a single function to help with future changes
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/dev-utils': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Switched the conditional `react-dom/client` import to use `import(...)` rather than `require(...)`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': patch
|
||||
---
|
||||
|
||||
Fixed a rare occurrence where a race in the search bar could throw away user input or cause the clear button not to work.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
If create app installs dependencies, don't suggest to user that they also need to do it.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
The experimental package detection will now ignore packages that don't make `package.json` available.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': minor
|
||||
---
|
||||
|
||||
The `UserListPicker` component has undergone improvements to enhance its performance.
|
||||
|
||||
The previous implementation inferred the number of owned and starred entities based on the entities available in the `EntityListContext`. The updated version no longer relies on the `EntityListContext` for inference, allowing for better decoupling.
|
||||
|
||||
The component now loads the entities' count asynchronously, resulting in improved performance and responsiveness. For this purpose, some of the exported filters such as `EntityTagFilter`, `EntityOwnerFilter`, `EntityLifecycleFilter` and `EntityNamespaceFilter` have now the `getCatalogFilters` method implemented.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Improve the extension boundary component and create a default extension suspense component.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Reverted the Microsoft auth provider to the previous implementation.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Added a workaround for double `default` wrapping when dynamically importing CommonJS modules with default exports.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Add examples for `github:webhook` scaffolder action & improve related tests
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Added `EXPERIMENTAL_VITE` flag for using [vite](https://vitejs.dev) as dev server instead of Webpack
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
Allow setting `update: true` in `publish:github:pull-request` scaffolder action
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Add component data `core.type` marker for `AppRouter` and `FlatRoutes`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
|
||||
---
|
||||
|
||||
Add examples for `gitlab:projectAccessToken:create` scaffolder action & improve related tests
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Ignore `stdin` when spawning backend child process for the `start` command. Fixing an issue where backend startup would hang.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Add examples for `publish:github:pull-request` scaffolder action & improve related tests
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': patch
|
||||
---
|
||||
|
||||
Use default extensions boundary and suspense on the alpha declarative `createSearchResultListItem` extension factory.
|
||||
@@ -430,6 +430,7 @@ Valentina
|
||||
validator
|
||||
validators
|
||||
varchar
|
||||
vite
|
||||
VMware
|
||||
Vodafone
|
||||
VPCs
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
ref: 'refs/pull/${{ github.event.pull_request.number }}/merge'
|
||||
- name: fetch base
|
||||
run: git fetch --depth 1 origin ${{ github.base_ref }}
|
||||
- uses: backstage/actions/changeset-feedback@v0.6.4
|
||||
- uses: backstage/actions/changeset-feedback@v0.6.5
|
||||
name: Generate feedback
|
||||
with:
|
||||
diff-ref: 'origin/master'
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: backstage/actions/cron@v0.6.4
|
||||
- uses: backstage/actions/cron@v0.6.5
|
||||
with:
|
||||
app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }}
|
||||
private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }}
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
node-version: 18.x
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@497722d45fb5a35c14c721f63ed74e6ad5f629dd # v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v18.x
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
|
||||
|
||||
@@ -15,4 +15,4 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Issue sync
|
||||
uses: backstage/actions/issue-sync@v0.6.4
|
||||
uses: backstage/actions/issue-sync@v0.6.5
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
const prNumber = artifact.name.slice('pr_number-'.length)
|
||||
core.setOutput('pr-number', prNumber);
|
||||
|
||||
- uses: backstage/actions/re-review@v0.6.4
|
||||
- uses: backstage/actions/re-review@v0.6.5
|
||||
with:
|
||||
app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }}
|
||||
private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }}
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: PR sync
|
||||
uses: backstage/actions/pr-sync@v0.6.4
|
||||
uses: backstage/actions/pr-sync@v0.6.5
|
||||
with:
|
||||
github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
|
||||
app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }}
|
||||
|
||||
@@ -66,6 +66,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
|
||||
uses: github/codeql-action/upload-sarif@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
node-version: 18.x
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v18.x
|
||||
|
||||
|
||||
@@ -58,6 +58,6 @@ jobs:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=7168
|
||||
- name: Upload Snyk report
|
||||
uses: github/codeql-action/upload-sarif@v2.21.8
|
||||
uses: github/codeql-action/upload-sarif@v2.22.4
|
||||
with:
|
||||
sarif_file: snyk.sarif
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: linux-v18
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v18.x
|
||||
- name: run Lighthouse CI
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2.21.8
|
||||
uses: github/codeql-action/init@v2.22.4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2.21.8
|
||||
uses: github/codeql-action/autobuild@v2.22.4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -80,4 +80,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2.21.8
|
||||
uses: github/codeql-action/analyze@v2.22.4
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
uses: browser-actions/setup-chrome@803ef6dfb4fdf22089c9563225d95e4a515820a0 # latest
|
||||
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@v0.6.4
|
||||
uses: backstage/actions/yarn-install@v0.6.5
|
||||
with:
|
||||
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
|
||||
- name: storybook yarn install
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
|
||||
| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates<br/> Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc |
|
||||
| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. |
|
||||
| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 |
|
||||
| [Keyloop](https://www.keyloop.com/) | [Andre Wanlin](https://github.com/awanlin) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 |
|
||||
| [Keyloop](https://www.keyloop.com/) | [Shawn Bruce](https://github.com/sbruce-keyloop) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 |
|
||||
| [Simply Business](https://sbtech.simplybusiness.co.uk/) | [@addersuk](https://github.com/addersuk), [@LightningStairs](https://github.com/LightningStairs), [@punitcse](https://github.com/punitcse), [@moltenice](https://github.com/moltenice) | Central developer portal to access everything a developer needs such as docs, internal service catalog, and the ability to quickly create a new service from a template. Internally developed Backstage plugins allow us to customise the experience to how we work. |
|
||||
| [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding |
|
||||
| [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. |
|
||||
|
||||
@@ -145,7 +145,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling.
|
||||
| ------------------------------ | ------------------------- | ----------------------------------------------------- | ------------------------------ |
|
||||
| Adam Harvey | Cisco | [adamdmharvey](https://github.com/adamdmharvey) | `adamharvey_` |
|
||||
| Alex Crome | | [afscrome](https://github.com/afscrome) | `afscrome` |
|
||||
| Andre Wanlin | Keyloop | [awanlin](https://github.com/awanlin) | `ahhhndre` |
|
||||
| Andre Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` |
|
||||
| Andrew Thauer | Wealthsimple | [andrewthauer](https://github.com/andrewthauer) | `andrewthauer#3060` |
|
||||
| Aramis Sennyey | Spotify | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` |
|
||||
| Brian Fletcher | Roadie.io | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` |
|
||||
|
||||
@@ -23,7 +23,7 @@ import scaffolderPlugin from '@backstage/plugin-scaffolder-backend';
|
||||
const backend = createBackend();
|
||||
|
||||
// Install desired features
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
|
||||
// Features can also be installed using an explicit reference
|
||||
backend.add(scaffolderPlugin());
|
||||
|
||||
@@ -24,9 +24,9 @@ import { createBackend } from '@backstage/backend-defaults'; // Omitted in the e
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-app-backend'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
backend.add(import('@backstage/plugin-app-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
@@ -126,8 +126,8 @@ You can now trim down the `src/index.ts` files to only include the plugins and m
|
||||
```ts
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-app-backend'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(import('@backstage/plugin-app-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
@@ -139,7 +139,7 @@ And `backend-b`, don't forget to clean up dependencies in `package.json` as well
|
||||
```ts
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.start();
|
||||
```
|
||||
|
||||
|
||||
@@ -177,11 +177,10 @@ custom API, so we use a helper function to transform that particular one.
|
||||
To make additions as mentioned above to the environment, you will start to get
|
||||
into the weeds of how the backend system wiring works. You'll need to have a
|
||||
service reference and a service factory that performs the actual creation of
|
||||
your service. Please see [the services
|
||||
article](../architecture/03-services.md#defining-a-service) to learn how to
|
||||
create a service ref and its default factory. You can place that code directly
|
||||
in the index file for now if you want, or near the actual implementation class
|
||||
in question.
|
||||
your service. Please see [the services article](../architecture/03-services.md)
|
||||
to learn how to create a service ref and its default factory. You can place that
|
||||
code directly in the index file for now if you want, or near the actual implementation
|
||||
class in question.
|
||||
|
||||
In this example, we'll assume that your added environment field is named
|
||||
`example`, and the created ref is named `exampleServiceRef`.
|
||||
@@ -233,7 +232,7 @@ be used in its new form.
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
const backend = createBackend();
|
||||
/* highlight-add-next-line */
|
||||
backend.add(import('@backstage/plugin-app-backend'));
|
||||
backend.add(import('@backstage/plugin-app-backend/alpha'));
|
||||
```
|
||||
|
||||
If you need to override the app package name, which otherwise defaults to `"app"`,
|
||||
@@ -248,7 +247,7 @@ A basic installation of the catalog plugin looks as follows.
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
const backend = createBackend();
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
@@ -296,7 +295,7 @@ const catalogModuleCustomExtensions = createBackendModule({
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
|
||||
);
|
||||
@@ -390,7 +389,7 @@ A basic installation of the scaffolder plugin looks as follows.
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
const backend = createBackend();
|
||||
/* highlight-add-next-line */
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
```
|
||||
|
||||
If you have other customizations made to `plugins/scaffolder.ts`, such as adding
|
||||
@@ -429,7 +428,7 @@ const scaffolderModuleCustomExtensions = createBackendModule({
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(scaffolderModuleCustomExtensions());
|
||||
```
|
||||
|
||||
@@ -22,7 +22,7 @@ Imagine you have a plugin that is responsible for storing FAQ snippets in a data
|
||||
|
||||
The search platform provides an interface (`DocumentCollatorFactory` from package `@backstage/plugin-search-common`) that allows you to do exactly that. It works by registering each of your entries as a "document" that later represents one search result each.
|
||||
|
||||
> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices.
|
||||
> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices.
|
||||
|
||||
#### 1. Install collator interface dependencies
|
||||
|
||||
|
||||
@@ -1299,7 +1299,7 @@
|
||||
- d3fea4ae0a: Internal fixes to avoid implicit usage of globals
|
||||
- 3280711113: Updated dependency `msw` to `^0.49.0`.
|
||||
- 9516b0c355: Added support for sending virtual pageviews on `search` events in order to enable
|
||||
Site Search functionality in GA. For more information consult [README](/plugins/analytics-module-ga/README.md#enabling-site-search)
|
||||
Site Search functionality in GA. For more information consult [README](https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md#enabling-site-search)
|
||||
- Updated dependencies
|
||||
- @backstage/core-plugin-api@1.2.0
|
||||
- @backstage/core-components@0.12.1
|
||||
|
||||
@@ -525,7 +525,7 @@
|
||||
### Patch Changes
|
||||
|
||||
- 9516b0c355: Added support for sending virtual pageviews on `search` events in order to enable
|
||||
Site Search functionality in GA. For more information consult [README](/plugins/analytics-module-ga/README.md#enabling-site-search)
|
||||
Site Search functionality in GA. For more information consult [README](https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md#enabling-site-search)
|
||||
- Updated dependencies
|
||||
- @backstage/core-plugin-api@1.2.0-next.2
|
||||
- @backstage/core-components@0.12.1-next.2
|
||||
|
||||
@@ -4,12 +4,17 @@ app:
|
||||
routes:
|
||||
bindings:
|
||||
plugin.pages.externalRoutes.pageX: plugin.pages.routes.pageX
|
||||
# waiting for https://github.com/backstage/backstage/pull/20605
|
||||
# catalog.externalRoutes.viewTechDoc: techdocs.routes.docRoot
|
||||
plugin.catalog.externalRoutes.viewTechDoc: plugin.techdocs.routes.docRoot
|
||||
|
||||
extensions:
|
||||
- apis.plugin.graphiql.browse.gitlab: true
|
||||
|
||||
# Entity page cards
|
||||
- 'entity.cards.about'
|
||||
|
||||
# Entity page content
|
||||
- 'entity.content.techdocs'
|
||||
|
||||
# scmAuthExtension: >-
|
||||
# createScmAuthExtension({
|
||||
# id: 'apis.scmAuth.addons.ghe',
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
"@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:^",
|
||||
|
||||
@@ -29,11 +29,8 @@ import {
|
||||
createExtension,
|
||||
createApiExtension,
|
||||
createExtensionOverrides,
|
||||
createPageExtension,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import techdocsPlugin from '@backstage/plugin-techdocs/alpha';
|
||||
import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { homePage } from './HomePage';
|
||||
import { collectLegacyRoutes } from '@backstage/core-compat-api';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
@@ -75,13 +72,6 @@ TODO:
|
||||
|
||||
/* app.tsx */
|
||||
|
||||
const entityPageExtension = createPageExtension({
|
||||
id: 'catalog:entity',
|
||||
defaultPath: '/catalog/:namespace/:kind/:name',
|
||||
routeRef: convertLegacyRouteRef(entityRouteRef),
|
||||
loader: async () => <div>Just a temporary mocked entity page</div>,
|
||||
});
|
||||
|
||||
const homePageExtension = createExtension({
|
||||
id: 'myhomepage',
|
||||
attachTo: { id: 'home', input: 'props' },
|
||||
@@ -122,12 +112,7 @@ const app = createApp({
|
||||
homePlugin,
|
||||
...collectedLegacyPlugins,
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
entityPageExtension,
|
||||
homePageExtension,
|
||||
scmAuthExtension,
|
||||
scmIntegrationApi,
|
||||
],
|
||||
extensions: [homePageExtension, scmAuthExtension, scmIntegrationApi],
|
||||
}),
|
||||
],
|
||||
/* Handled through config instead */
|
||||
|
||||
@@ -93,12 +93,16 @@
|
||||
"@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",
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"history": "^5.0.0",
|
||||
"react": "^18.0.2",
|
||||
"react-dom": "^18.0.2",
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-use": "^17.2.4",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-node-polyfills": "^0.14.1",
|
||||
"zen-observable": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
EntityAzurePullRequestsContent,
|
||||
isAzureDevOpsAvailable,
|
||||
isAzurePipelinesAvailable,
|
||||
EntityAzureReadmeCard,
|
||||
} from '@backstage/plugin-azure-devops';
|
||||
import {
|
||||
isOctopusDeployAvailable,
|
||||
@@ -415,6 +416,14 @@ const overviewContent = (
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isAzureDevOpsAvailable}>
|
||||
<Grid item md={6}>
|
||||
<EntityAzureReadmeCard />
|
||||
</Grid>
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
|
||||
<Grid item md={2}>
|
||||
<InfoCard title="Rate this entity">
|
||||
<LikeDislikeButtons />
|
||||
|
||||
@@ -57,7 +57,26 @@ function isPromise<T>(value: unknown | Promise<T>): value is Promise<T> {
|
||||
}
|
||||
|
||||
function unwrapFeature(
|
||||
feature: BackendFeature | (() => BackendFeature),
|
||||
feature:
|
||||
| BackendFeature
|
||||
| (() => BackendFeature)
|
||||
| { default: BackendFeature | (() => BackendFeature) },
|
||||
): BackendFeature {
|
||||
return typeof feature === 'function' ? feature() : feature;
|
||||
if (typeof feature === 'function') {
|
||||
return feature();
|
||||
}
|
||||
if ('$$type' in feature) {
|
||||
return feature;
|
||||
}
|
||||
// This is a workaround where default exports get transpiled to `exports['default'] = ...`
|
||||
// in CommonJS modules, which in turn results in a double `{ default: { default: ... } }` nesting
|
||||
// when importing using a dynamic import.
|
||||
// TODO: This is a broader issue than just this piece of code, and should move away from CommonJS.
|
||||
if ('default' in feature) {
|
||||
const defaultFeature = feature.default;
|
||||
return typeof defaultFeature === 'function'
|
||||
? defaultFeature()
|
||||
: defaultFeature;
|
||||
}
|
||||
return feature;
|
||||
}
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/release-manifests": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@esbuild-kit/cjs-loader": "^2.4.1",
|
||||
"@esbuild-kit/esm-loader": "^2.5.5",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"@octokit/graphql": "^5.0.0",
|
||||
"@octokit/graphql-schema": "^13.7.0",
|
||||
@@ -131,6 +129,7 @@
|
||||
"swc-loader": "^0.2.3",
|
||||
"tar": "^6.1.12",
|
||||
"terser-webpack-plugin": "^5.1.3",
|
||||
"tsx": "^3.14.0",
|
||||
"util": "^0.12.3",
|
||||
"webpack": "^5.70.0",
|
||||
"webpack-dev-server": "^4.7.3",
|
||||
@@ -153,6 +152,7 @@
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"@types/diff": "^5.0.0",
|
||||
"@types/ejs": "^3.1.3",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
@@ -174,11 +174,27 @@
|
||||
"type-fest": "^2.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@microsoft/api-extractor": "^7.21.2"
|
||||
"@microsoft/api-extractor": "^7.21.2",
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-node-polyfills": "^0.14.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@microsoft/api-extractor": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/plugin-react": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
},
|
||||
"vite-plugin-html": {
|
||||
"optional": true
|
||||
},
|
||||
"vite-plugin-node-polyfills": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -39,6 +39,7 @@ import { runPlain } from '../run';
|
||||
import { transforms } from './transforms';
|
||||
import { version } from '../../lib/version';
|
||||
import yn from 'yn';
|
||||
import { hasReactDomClient } from './hasReactDomClient';
|
||||
|
||||
const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE';
|
||||
|
||||
@@ -81,15 +82,6 @@ async function readBuildInfo() {
|
||||
};
|
||||
}
|
||||
|
||||
function hasReactDomClient() {
|
||||
try {
|
||||
require.resolve('react-dom/client');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createConfig(
|
||||
paths: BundlingPaths,
|
||||
options: BundlingOptions,
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
export function hasReactDomClient() {
|
||||
try {
|
||||
require.resolve('react-dom/client');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -77,24 +77,28 @@ async function detectPackages(
|
||||
return [];
|
||||
}
|
||||
|
||||
const depPackageJson: BackstagePackageJson = require(require.resolve(
|
||||
`${depName}/package.json`,
|
||||
{ paths: [targetPath] },
|
||||
));
|
||||
if (
|
||||
['frontend-plugin', 'frontend-plugin-module'].includes(
|
||||
depPackageJson.backstage?.role ?? '',
|
||||
)
|
||||
) {
|
||||
// Include alpha entry point if available. If there's no default export it will be ignored
|
||||
const exp = depPackageJson.exports;
|
||||
if (exp && typeof exp === 'object' && './alpha' in exp) {
|
||||
return [
|
||||
{ name: depName, import: depName },
|
||||
{ name: depName, export: './alpha', import: `${depName}/alpha` },
|
||||
];
|
||||
try {
|
||||
const depPackageJson: BackstagePackageJson = require(require.resolve(
|
||||
`${depName}/package.json`,
|
||||
{ paths: [targetPath] },
|
||||
));
|
||||
if (
|
||||
['frontend-plugin', 'frontend-plugin-module'].includes(
|
||||
depPackageJson.backstage?.role ?? '',
|
||||
)
|
||||
) {
|
||||
// Include alpha entry point if available. If there's no default export it will be ignored
|
||||
const exp = depPackageJson.exports;
|
||||
if (exp && typeof exp === 'object' && './alpha' in exp) {
|
||||
return [
|
||||
{ name: depName, import: depName },
|
||||
{ name: depName, export: './alpha', import: `${depName}/alpha` },
|
||||
];
|
||||
}
|
||||
return [{ name: depName, import: depName }];
|
||||
}
|
||||
return [{ name: depName, import: depName }];
|
||||
} catch {
|
||||
/* ignore packages that don't make package.json available */
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ import { createConfig, resolveBaseUrl } from './config';
|
||||
import { createDetectedModulesEntryPoint } from './packageDetection';
|
||||
import { resolveBundlingPaths } from './paths';
|
||||
import { ServeOptions } from './types';
|
||||
import { hasReactDomClient } from './hasReactDomClient';
|
||||
|
||||
export async function serveBundle(options: ServeOptions) {
|
||||
const paths = resolveBundlingPaths(options);
|
||||
@@ -77,7 +78,9 @@ export async function serveBundle(options: ServeOptions) {
|
||||
|
||||
const { name } = await fs.readJson(libPaths.resolveTarget('package.json'));
|
||||
|
||||
let server: WebpackDevServer | undefined = undefined;
|
||||
let webpackServer: WebpackDevServer | undefined = undefined;
|
||||
let viteServer: import('vite').ViteDevServer | undefined = undefined;
|
||||
|
||||
let latestFrontendAppConfigs: AppConfig[] = [];
|
||||
|
||||
const cliConfig = await loadCliConfig({
|
||||
@@ -86,7 +89,9 @@ export async function serveBundle(options: ServeOptions) {
|
||||
withFilteredKeys: true,
|
||||
watch(appConfigs) {
|
||||
latestFrontendAppConfigs = appConfigs;
|
||||
server?.invalidate();
|
||||
|
||||
webpackServer?.invalidate();
|
||||
viteServer?.restart();
|
||||
},
|
||||
});
|
||||
latestFrontendAppConfigs = cliConfig.frontendAppConfigs;
|
||||
@@ -123,7 +128,8 @@ export async function serveBundle(options: ServeOptions) {
|
||||
config: fullConfig,
|
||||
targetPath: paths.targetPath,
|
||||
watch() {
|
||||
server?.invalidate();
|
||||
webpackServer?.invalidate();
|
||||
viteServer?.restart();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -139,64 +145,111 @@ export async function serveBundle(options: ServeOptions) {
|
||||
additionalEntryPoints: detectedModulesEntryPoint,
|
||||
});
|
||||
|
||||
const compiler = webpack(config);
|
||||
|
||||
server = new WebpackDevServer(
|
||||
{
|
||||
hot: !process.env.CI,
|
||||
devMiddleware: {
|
||||
publicPath: config.output?.publicPath as string,
|
||||
stats: 'errors-warnings',
|
||||
if (process.env.EXPERIMENTAL_VITE) {
|
||||
const { default: vite } = await import('vite');
|
||||
const { default: viteReact } = await import('@vitejs/plugin-react');
|
||||
const { nodePolyfills: viteNodePolyfills } = await import(
|
||||
'vite-plugin-node-polyfills'
|
||||
);
|
||||
const { createHtmlPlugin: viteHtml } = await import('vite-plugin-html');
|
||||
viteServer = await vite.createServer({
|
||||
define: {
|
||||
global: 'window',
|
||||
'process.argv': JSON.stringify(process.argv),
|
||||
'process.env.APP_CONFIG': JSON.stringify(cliConfig.frontendAppConfigs),
|
||||
// This allows for conditional imports of react-dom/client, since there's no way
|
||||
// to check for presence of it in source code without module resolution errors.
|
||||
'process.env.HAS_REACT_DOM_CLIENT': JSON.stringify(hasReactDomClient()),
|
||||
},
|
||||
static: paths.targetPublic
|
||||
? {
|
||||
publicPath: config.output?.publicPath as string,
|
||||
directory: paths.targetPublic,
|
||||
}
|
||||
: undefined,
|
||||
historyApiFallback: {
|
||||
// Paths with dots should still use the history fallback.
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/387.
|
||||
disableDotRule: true,
|
||||
|
||||
// The index needs to be rewritten relative to the new public path, including subroutes.
|
||||
index: `${config.output?.publicPath}index.html`,
|
||||
plugins: [
|
||||
viteReact(),
|
||||
viteNodePolyfills(),
|
||||
viteHtml({
|
||||
entry: paths.targetEntry,
|
||||
// todo(blam): we should look at contributing to thPe plugin here
|
||||
// to support absolute paths, but works in the interim at least.
|
||||
template: 'public/index.html',
|
||||
inject: {
|
||||
data: {
|
||||
config: frontendConfig,
|
||||
publicPath: config.output?.publicPath,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
host,
|
||||
port,
|
||||
},
|
||||
https:
|
||||
url.protocol === 'https:'
|
||||
? {
|
||||
cert: fullConfig.getString('app.https.certificate.cert'),
|
||||
key: fullConfig.getString('app.https.certificate.key'),
|
||||
}
|
||||
: false,
|
||||
host,
|
||||
port,
|
||||
proxy: targetPkg.proxy,
|
||||
// When the dev server is behind a proxy, the host and public hostname differ
|
||||
allowedHosts: [url.hostname],
|
||||
client: {
|
||||
webSocketURL: 'auto://0.0.0.0:0/ws',
|
||||
},
|
||||
} as any,
|
||||
compiler as any,
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server?.startCallback((err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
openBrowser(url.href);
|
||||
resolve();
|
||||
publicDir: paths.targetPublic,
|
||||
root: paths.targetPath,
|
||||
});
|
||||
} else {
|
||||
const compiler = webpack(config);
|
||||
|
||||
webpackServer = new WebpackDevServer(
|
||||
{
|
||||
hot: !process.env.CI,
|
||||
devMiddleware: {
|
||||
publicPath: config.output?.publicPath as string,
|
||||
stats: 'errors-warnings',
|
||||
},
|
||||
static: paths.targetPublic
|
||||
? {
|
||||
publicPath: config.output?.publicPath as string,
|
||||
directory: paths.targetPublic,
|
||||
}
|
||||
: undefined,
|
||||
historyApiFallback: {
|
||||
// Paths with dots should still use the history fallback.
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/387.
|
||||
disableDotRule: true,
|
||||
|
||||
// The index needs to be rewritten relative to the new public path, including subroutes.
|
||||
index: `${config.output?.publicPath}index.html`,
|
||||
},
|
||||
https:
|
||||
url.protocol === 'https:'
|
||||
? {
|
||||
cert: fullConfig.getString('app.https.certificate.cert'),
|
||||
key: fullConfig.getString('app.https.certificate.key'),
|
||||
}
|
||||
: false,
|
||||
host,
|
||||
port,
|
||||
proxy: targetPkg.proxy,
|
||||
// When the dev server is behind a proxy, the host and public hostname differ
|
||||
allowedHosts: [url.hostname],
|
||||
client: {
|
||||
webSocketURL: 'auto://0.0.0.0:0/ws',
|
||||
},
|
||||
},
|
||||
compiler,
|
||||
);
|
||||
}
|
||||
|
||||
await viteServer?.listen();
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
if (webpackServer) {
|
||||
webpackServer.startCallback((err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
openBrowser(url.href);
|
||||
|
||||
const waitForExit = async () => {
|
||||
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
||||
process.on(signal, () => {
|
||||
server?.close();
|
||||
webpackServer?.close();
|
||||
viteServer?.close();
|
||||
// exit instead of resolve. The process is shutting down and resolving a promise here logs an error
|
||||
process.exit();
|
||||
});
|
||||
|
||||
@@ -27,11 +27,14 @@ import { isAbsolute as isAbsolutePath } from 'path';
|
||||
import { paths } from '../paths';
|
||||
import spawn from 'cross-spawn';
|
||||
|
||||
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
|
||||
const supportsModuleLoaderRegister = nodeMajor >= 20 && nodeMinor >= 6;
|
||||
|
||||
const loaderArgs = [
|
||||
'--require',
|
||||
require.resolve('@esbuild-kit/cjs-loader'),
|
||||
'--loader',
|
||||
pathToFileURL(require.resolve('@esbuild-kit/esm-loader')).toString(), // Windows prefers a URL here
|
||||
require.resolve('tsx/preflight'),
|
||||
supportsModuleLoaderRegister ? '--import' : '--loader',
|
||||
pathToFileURL(require.resolve('tsx')).toString(), // Windows prefers a URL here
|
||||
];
|
||||
|
||||
export async function startBackendExperimental(options: BackendServeOptions) {
|
||||
@@ -95,7 +98,7 @@ export async function startBackendExperimental(options: BackendServeOptions) {
|
||||
process.execPath,
|
||||
[...loaderArgs, ...optionArgs, options.entry, ...userArgs],
|
||||
{
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
||||
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
||||
env: {
|
||||
...process.env,
|
||||
BACKSTAGE_CLI_CHANNEL: '1',
|
||||
|
||||
@@ -95,6 +95,14 @@ describe('ConfigSources', () => {
|
||||
),
|
||||
).toEqual([{ name: 'FileConfigSource', path: '/config.yaml' }]);
|
||||
|
||||
expect(
|
||||
mergeSources(
|
||||
ConfigSources.defaultForTargets({
|
||||
targets: [{ type: 'path', target: 'config.yaml' }],
|
||||
}),
|
||||
),
|
||||
).toEqual([{ name: 'FileConfigSource', path: resolvePath('config.yaml') }]);
|
||||
|
||||
const subFunc = async () => undefined;
|
||||
expect(
|
||||
mergeSources(
|
||||
@@ -172,8 +180,8 @@ describe('ConfigSources', () => {
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
{ name: 'FileConfigSource', path: 'a.yaml' },
|
||||
{ name: 'FileConfigSource', path: 'b.yaml' },
|
||||
{ name: 'FileConfigSource', path: resolvePath('a.yaml') },
|
||||
{ name: 'FileConfigSource', path: resolvePath('b.yaml') },
|
||||
{ name: 'EnvConfigSource', env: { HOME: '/' } },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ export class ConfigSources {
|
||||
}
|
||||
return FileConfigSource.create({
|
||||
watch: options.watch,
|
||||
path: arg.target,
|
||||
path: resolvePath(arg.target),
|
||||
substitutionFunc: options.substitutionFunc,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import React, { useContext, ReactNode, ComponentType, useState } from 'react';
|
||||
import {
|
||||
attachComponentData,
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
IdentityApi,
|
||||
@@ -186,3 +187,5 @@ export function AppRouter(props: AppRouterProps) {
|
||||
</RouterComponent>
|
||||
);
|
||||
}
|
||||
|
||||
attachComponentData(AppRouter, 'core.type', 'AppRouter');
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
import { useApp, useElementFilter } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
attachComponentData,
|
||||
useApp,
|
||||
useElementFilter,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { isReactRouterBeta } from '../app/isReactRouterBeta';
|
||||
|
||||
let warned = false;
|
||||
@@ -115,3 +119,5 @@ export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => {
|
||||
|
||||
return useRoutes(withNotFound);
|
||||
};
|
||||
|
||||
attachComponentData(FlatRoutes, 'core.type', 'FlatRoutes');
|
||||
|
||||
@@ -6,11 +6,18 @@
|
||||
/// <reference types="react" />
|
||||
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionOverrides } from '@backstage/frontend-plugin-api';
|
||||
import { default as React_2 } from 'react';
|
||||
|
||||
// @public (undocumented)
|
||||
export function collectLegacyRoutes(
|
||||
flatRoutesElement: JSX.Element,
|
||||
): BackstagePlugin[];
|
||||
|
||||
// @public (undocumented)
|
||||
export function convertLegacyApp(
|
||||
rootElement: React_2.JSX.Element,
|
||||
): (ExtensionOverrides | BackstagePlugin)[];
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('collectLegacyRoutes', () => {
|
||||
<Route path="/score-board" element={<ScoreBoardPage />} />
|
||||
<Route path="/stackstorm" element={<StackstormPage />} />
|
||||
<Route path="/puppetdb" element={<PuppetDbPage />} />
|
||||
<Route path="/puppetdb" element={<PuppetDbPage />} />
|
||||
</FlatRoutes>,
|
||||
);
|
||||
|
||||
@@ -85,6 +86,12 @@ describe('collectLegacyRoutes', () => {
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'puppetdb' },
|
||||
},
|
||||
{
|
||||
id: 'plugin.puppetDb.page2',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'puppetdb' },
|
||||
},
|
||||
{
|
||||
id: 'apis.plugin.puppetdb.service',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
|
||||
@@ -62,7 +62,10 @@ Existing tasks:
|
||||
export function collectLegacyRoutes(
|
||||
flatRoutesElement: JSX.Element,
|
||||
): BackstagePlugin[] {
|
||||
const results = new Array<BackstagePlugin>();
|
||||
const createdPluginIds = new Map<
|
||||
LegacyBackstagePlugin,
|
||||
Extension<unknown>[]
|
||||
>();
|
||||
|
||||
React.Children.forEach(
|
||||
flatRoutesElement.props.children,
|
||||
@@ -93,13 +96,18 @@ export function collectLegacyRoutes(
|
||||
);
|
||||
|
||||
const pluginId = plugin.getId();
|
||||
const path: string = route.props.path;
|
||||
|
||||
const detectedExtensions = new Array<Extension<unknown>>();
|
||||
const detectedExtensions =
|
||||
createdPluginIds.get(plugin) ?? new Array<Extension<unknown>>();
|
||||
createdPluginIds.set(plugin, detectedExtensions);
|
||||
|
||||
const path: string = route.props.path;
|
||||
|
||||
detectedExtensions.push(
|
||||
createPageExtension({
|
||||
id: `plugin.${pluginId}.page`,
|
||||
id: `plugin.${pluginId}.page${
|
||||
detectedExtensions.length ? detectedExtensions.length + 1 : ''
|
||||
}`,
|
||||
defaultPath: path[0] === '/' ? path.slice(1) : path,
|
||||
routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined,
|
||||
|
||||
@@ -115,23 +123,20 @@ export function collectLegacyRoutes(
|
||||
),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
detectedExtensions.push(
|
||||
return Array.from(createdPluginIds).map(([plugin, extensions]) =>
|
||||
createPlugin({
|
||||
id: plugin.getId(),
|
||||
extensions: [
|
||||
...extensions,
|
||||
...Array.from(plugin.getApis()).map(factory =>
|
||||
createApiExtension({
|
||||
factory,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
results.push(
|
||||
createPlugin({
|
||||
id: plugin.getId(),
|
||||
extensions: detectedExtensions,
|
||||
}),
|
||||
);
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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 { AppRouter, FlatRoutes } from '@backstage/core-app-api';
|
||||
import { PuppetDbPage } from '@backstage/plugin-puppetdb';
|
||||
import { StackstormPage } from '@backstage/plugin-stackstorm';
|
||||
import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { convertLegacyApp } from './convertLegacyApp';
|
||||
|
||||
const Root = ({ children }: { children: ReactNode }) => <>{children}</>;
|
||||
|
||||
describe('convertLegacyApp', () => {
|
||||
it('should find and extract root and routes', () => {
|
||||
const collected = convertLegacyApp(
|
||||
<>
|
||||
<div />
|
||||
<span />
|
||||
<AppRouter>
|
||||
<div />
|
||||
<Root>
|
||||
<FlatRoutes>
|
||||
<Route path="/score-board" element={<ScoreBoardPage />} />
|
||||
<Route path="/stackstorm" element={<StackstormPage />} />
|
||||
<Route path="/puppetdb" element={<PuppetDbPage />} />
|
||||
<Route path="/puppetdb" element={<PuppetDbPage />} />
|
||||
</FlatRoutes>
|
||||
</Root>
|
||||
</AppRouter>
|
||||
</>,
|
||||
);
|
||||
|
||||
expect(
|
||||
collected.map((p: any /* TODO */) => ({
|
||||
id: p.id,
|
||||
extensions: p.extensions.map((e: any) => ({
|
||||
id: e.id,
|
||||
attachTo: e.attachTo,
|
||||
disabled: e.disabled,
|
||||
defaultConfig: e.configSchema?.parse({}),
|
||||
})),
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
id: 'score-card',
|
||||
extensions: [
|
||||
{
|
||||
id: 'plugin.score-card.page',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'score-board' },
|
||||
},
|
||||
{
|
||||
id: 'apis.plugin.scoringdata.service',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'stackstorm',
|
||||
extensions: [
|
||||
{
|
||||
id: 'plugin.stackstorm.page',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'stackstorm' },
|
||||
},
|
||||
{
|
||||
id: 'apis.plugin.stackstorm.service',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'puppetDb',
|
||||
extensions: [
|
||||
{
|
||||
id: 'plugin.puppetDb.page',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'puppetdb' },
|
||||
},
|
||||
{
|
||||
id: 'plugin.puppetDb.page2',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'puppetdb' },
|
||||
},
|
||||
{
|
||||
id: 'apis.plugin.puppetdb.service',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: undefined,
|
||||
extensions: [
|
||||
{
|
||||
id: 'core.layout',
|
||||
attachTo: { id: 'core', input: 'root' },
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: 'core.nav',
|
||||
attachTo: { id: 'core.layout', input: 'nav' },
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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, {
|
||||
Children,
|
||||
Fragment,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
isValidElement,
|
||||
} from 'react';
|
||||
import {
|
||||
BackstagePlugin,
|
||||
ExtensionOverrides,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionInput,
|
||||
createExtensionOverrides,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { getComponentData } from '@backstage/core-plugin-api';
|
||||
import { collectLegacyRoutes } from './collectLegacyRoutes';
|
||||
|
||||
function selectChildren(
|
||||
rootNode: ReactNode,
|
||||
selector?: (element: ReactElement<{ children?: ReactNode }>) => boolean,
|
||||
strictError?: string,
|
||||
): Array<ReactElement<{ children?: ReactNode }>> {
|
||||
return Children.toArray(rootNode).flatMap(node => {
|
||||
if (!isValidElement<{ children?: ReactNode }>(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (node.type === Fragment) {
|
||||
return selectChildren(node.props.children, selector, strictError);
|
||||
}
|
||||
|
||||
if (selector === undefined || selector(node)) {
|
||||
return [node];
|
||||
}
|
||||
|
||||
if (strictError) {
|
||||
throw new Error(strictError);
|
||||
}
|
||||
|
||||
return selectChildren(node.props.children, selector, strictError);
|
||||
});
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function convertLegacyApp(
|
||||
rootElement: React.JSX.Element,
|
||||
): (ExtensionOverrides | BackstagePlugin)[] {
|
||||
const appRouterEls = selectChildren(
|
||||
rootElement,
|
||||
el => getComponentData(el, 'core.type') === 'AppRouter',
|
||||
);
|
||||
if (appRouterEls.length !== 1) {
|
||||
throw new Error(
|
||||
"Failed to convert legacy app, AppRouter element could not been found. Make sure it's at the top level of the App element tree",
|
||||
);
|
||||
}
|
||||
|
||||
const rootEls = selectChildren(
|
||||
appRouterEls[0].props.children,
|
||||
el =>
|
||||
Boolean(el.props.children) &&
|
||||
selectChildren(
|
||||
el.props.children,
|
||||
innerEl => getComponentData(innerEl, 'core.type') === 'FlatRoutes',
|
||||
).length === 1,
|
||||
);
|
||||
if (rootEls.length !== 1) {
|
||||
throw new Error(
|
||||
"Failed to convert legacy app, Root element containing FlatRoutes could not been found. Make sure it's within the AppRouter element of the App element tree",
|
||||
);
|
||||
}
|
||||
const [rootEl] = rootEls;
|
||||
|
||||
const routesEls = selectChildren(
|
||||
rootEls[0].props.children,
|
||||
el => getComponentData(el, 'core.type') === 'FlatRoutes',
|
||||
);
|
||||
if (routesEls.length !== 1) {
|
||||
throw new Error(
|
||||
'Unexpectedly failed to find FlatRoutes in app element tree',
|
||||
);
|
||||
}
|
||||
const [routesEl] = routesEls;
|
||||
|
||||
const CoreLayoutOverride = createExtension({
|
||||
id: 'core.layout',
|
||||
attachTo: { id: 'core', input: 'root' },
|
||||
inputs: {
|
||||
content: createExtensionInput(
|
||||
{
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory({ bind, inputs }) {
|
||||
// Clone the root element, this replaces the FlatRoutes declared in the app with out content input
|
||||
bind({
|
||||
element: React.cloneElement(rootEl, undefined, inputs.content.element),
|
||||
});
|
||||
},
|
||||
});
|
||||
const CoreNavOverride = createExtension({
|
||||
id: 'core.nav',
|
||||
attachTo: { id: 'core.layout', input: 'nav' },
|
||||
output: {},
|
||||
factory() {},
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
const collectedRoutes = collectLegacyRoutes(routesEl);
|
||||
|
||||
return [
|
||||
...collectedRoutes,
|
||||
createExtensionOverrides({
|
||||
extensions: [CoreLayoutOverride, CoreNavOverride],
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -14,3 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { collectLegacyRoutes } from './collectLegacyRoutes';
|
||||
export { convertLegacyApp } from './convertLegacyApp';
|
||||
|
||||
@@ -27,8 +27,8 @@ import { SubRoute } from './types';
|
||||
|
||||
export function useSelectedSubRoute(subRoutes: SubRoute[]): {
|
||||
index: number;
|
||||
route: SubRoute;
|
||||
element: JSX.Element;
|
||||
route?: SubRoute;
|
||||
element?: JSX.Element;
|
||||
} {
|
||||
const params = useParams();
|
||||
|
||||
@@ -44,7 +44,7 @@ export function useSelectedSubRoute(subRoutes: SubRoute[]): {
|
||||
b.path.replace(/\/\*$/, '').localeCompare(a.path.replace(/\/\*$/, '')),
|
||||
);
|
||||
|
||||
const element = useRoutes(sortedRoutes) ?? subRoutes[0].children;
|
||||
const element = useRoutes(sortedRoutes) ?? subRoutes[0]?.children;
|
||||
|
||||
// TODO(Rugvip): Once we only support v6 stable we can always prefix
|
||||
// This avoids having a double / prefix for react-router v6 beta, which in turn breaks
|
||||
@@ -98,7 +98,7 @@ export function RoutedTabs(props: { routes: SubRoute[] }) {
|
||||
onChange={onTabChange}
|
||||
/>
|
||||
<Content>
|
||||
<Helmet title={route.title} />
|
||||
<Helmet title={route?.title} />
|
||||
{element}
|
||||
</Content>
|
||||
</>
|
||||
|
||||
@@ -129,7 +129,7 @@ export default async (opts: OptionValues): Promise<void> => {
|
||||
);
|
||||
Task.log();
|
||||
Task.section('All set! Now you might want to');
|
||||
if (!opts.skipInstall) {
|
||||
if (opts.skipInstall) {
|
||||
Task.log(
|
||||
` Install the dependencies: ${chalk.cyan(
|
||||
`cd ${opts.path ?? answers.name} && yarn install`,
|
||||
|
||||
@@ -49,11 +49,13 @@ import { createRoutesFromChildren, Route } from 'react-router-dom';
|
||||
import { SidebarThemeSwitcher } from './SidebarThemeSwitcher';
|
||||
import 'react-dom';
|
||||
|
||||
let ReactDOM: typeof import('react-dom') | typeof import('react-dom/client');
|
||||
let ReactDOMPromise: Promise<
|
||||
typeof import('react-dom') | typeof import('react-dom/client')
|
||||
>;
|
||||
if (process.env.HAS_REACT_DOM_CLIENT) {
|
||||
ReactDOM = require('react-dom/client');
|
||||
ReactDOMPromise = import('react-dom/client');
|
||||
} else {
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMPromise = import('react-dom');
|
||||
}
|
||||
|
||||
export function isReactRouterBeta(): boolean {
|
||||
@@ -242,11 +244,15 @@ export class DevAppBuilder {
|
||||
window.location.pathname = this.defaultPage;
|
||||
}
|
||||
|
||||
if ('createRoot' in ReactDOM) {
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<DevApp />);
|
||||
} else {
|
||||
ReactDOM.render(<DevApp />, document.getElementById('root'));
|
||||
}
|
||||
ReactDOMPromise.then(ReactDOM => {
|
||||
if ('createRoot' in ReactDOM) {
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<DevApp />,
|
||||
);
|
||||
} else {
|
||||
ReactDOM.render(<DevApp />, document.getElementById('root'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -34,17 +34,17 @@ export interface Config {
|
||||
/**
|
||||
* @deepVisibility frontend
|
||||
*/
|
||||
extensions?:
|
||||
extensions?: Array<
|
||||
| string
|
||||
| {
|
||||
[extensionId: string]:
|
||||
| boolean
|
||||
| string
|
||||
| {
|
||||
at?: string;
|
||||
extension?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
config?: unknown;
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
|
||||
export const Core = createExtension({
|
||||
id: 'core',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
attachTo: { id: 'root', input: 'default' }, // ignored
|
||||
inputs: {
|
||||
apis: createExtensionInput({
|
||||
api: coreExtensionData.apiFactory,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 {
|
||||
createExtension,
|
||||
createExtensionOverrides,
|
||||
createPlugin,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
import { createAppGraph } from './createAppGraph';
|
||||
|
||||
const extBase = {
|
||||
id: 'test',
|
||||
attachTo: { id: 'core', input: 'root' },
|
||||
output: {},
|
||||
factory() {},
|
||||
};
|
||||
|
||||
describe('createAppGraph', () => {
|
||||
it('throws an error when a core extension is parametrized', () => {
|
||||
const config = new MockConfigApi({
|
||||
app: {
|
||||
extensions: [
|
||||
{
|
||||
core: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const features = [
|
||||
createPlugin({
|
||||
id: 'plugin',
|
||||
extensions: [],
|
||||
}),
|
||||
];
|
||||
expect(() =>
|
||||
createAppGraph({ features, config, builtinExtensions: [] }),
|
||||
).toThrow("Configuration of the 'core' extension is forbidden");
|
||||
});
|
||||
|
||||
it('throws an error when a core extension is overridden', () => {
|
||||
const config = new MockConfigApi({});
|
||||
const features = [
|
||||
createPlugin({
|
||||
id: 'plugin',
|
||||
extensions: [
|
||||
createExtension({
|
||||
id: 'core',
|
||||
attachTo: { id: 'core.routes', input: 'route' },
|
||||
inputs: {},
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
expect(() =>
|
||||
createAppGraph({ features, config, builtinExtensions: [] }),
|
||||
).toThrow(
|
||||
"It is forbidden to override the following extension(s): 'core', which is done by the following plugin(s): 'plugin'",
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when duplicated extensions are detected', () => {
|
||||
const config = new MockConfigApi({});
|
||||
|
||||
const ExtensionA = createExtension({ ...extBase, id: 'A' });
|
||||
|
||||
const ExtensionB = createExtension({ ...extBase, id: 'B' });
|
||||
|
||||
const PluginA = createPlugin({
|
||||
id: 'A',
|
||||
extensions: [ExtensionA, ExtensionA],
|
||||
});
|
||||
|
||||
const PluginB = createPlugin({
|
||||
id: 'B',
|
||||
extensions: [ExtensionA, ExtensionB, ExtensionB],
|
||||
});
|
||||
|
||||
const features = [PluginA, PluginB];
|
||||
|
||||
expect(() =>
|
||||
createAppGraph({ features, config, builtinExtensions: [] }),
|
||||
).toThrow(
|
||||
"The following extensions are duplicated: The extension 'A' was provided 2 time(s) by the plugin 'A' and 1 time(s) by the plugin 'B', The extension 'B' was provided 2 time(s) by the plugin 'B'",
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when duplicated extension overrides are detected', () => {
|
||||
expect(() =>
|
||||
createAppGraph({
|
||||
features: [
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
createExtension({ ...extBase, id: 'a' }),
|
||||
createExtension({ ...extBase, id: 'a' }),
|
||||
createExtension({ ...extBase, id: 'b' }),
|
||||
],
|
||||
}),
|
||||
createExtensionOverrides({
|
||||
extensions: [createExtension({ ...extBase, id: 'b' })],
|
||||
}),
|
||||
],
|
||||
config: new MockConfigApi({}),
|
||||
builtinExtensions: [],
|
||||
}),
|
||||
).toThrow('The following extensions had duplicate overrides: a, b');
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
BackstagePlugin,
|
||||
Extension,
|
||||
ExtensionOverrides,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { readAppExtensionsConfig } from './readAppExtensionsConfig';
|
||||
import { resolveAppGraph } from './resolveAppGraph';
|
||||
import { resolveAppNodeSpecs } from './resolveAppNodeSpecs';
|
||||
import { AppGraph } from './types';
|
||||
import { Config } from '@backstage/config';
|
||||
import { instantiateAppNodeTree } from './instantiateAppNodeTree';
|
||||
|
||||
/** @internal */
|
||||
export interface CreateAppGraphOptions {
|
||||
features: (BackstagePlugin | ExtensionOverrides)[];
|
||||
builtinExtensions: Extension<unknown>[];
|
||||
config: Config;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createAppGraph(options: CreateAppGraphOptions): AppGraph {
|
||||
const appGraph = resolveAppGraph(
|
||||
'core',
|
||||
resolveAppNodeSpecs({
|
||||
features: options.features,
|
||||
builtinExtensions: options.builtinExtensions,
|
||||
parameters: readAppExtensionsConfig(options.config),
|
||||
forbidden: new Set(['core']),
|
||||
}),
|
||||
);
|
||||
instantiateAppNodeTree(appGraph.root);
|
||||
return appGraph;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export type {
|
||||
AppNode,
|
||||
AppNodeEdges,
|
||||
AppNodeInstance,
|
||||
AppNodeSpec,
|
||||
} from './types';
|
||||
export { createAppGraph } from './createAppGraph';
|
||||
@@ -0,0 +1,554 @@
|
||||
/*
|
||||
* 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,
|
||||
createExtension,
|
||||
createExtensionDataRef,
|
||||
createExtensionInput,
|
||||
createSchemaFromZod,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
createAppNodeInstance,
|
||||
instantiateAppNodeTree,
|
||||
} from './instantiateAppNodeTree';
|
||||
import { AppNodeInstance, AppNodeSpec } from './types';
|
||||
import { resolveAppGraph } from './resolveAppGraph';
|
||||
|
||||
const testDataRef = createExtensionDataRef<string>('test');
|
||||
const otherDataRef = createExtensionDataRef<number>('other');
|
||||
const inputMirrorDataRef = createExtensionDataRef<unknown>('mirror');
|
||||
|
||||
const simpleExtension = createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
output: z.string().default('test'),
|
||||
other: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
factory({ bind, config }) {
|
||||
bind({ test: config.output, other: config.other });
|
||||
},
|
||||
});
|
||||
|
||||
function makeSpec<TConfig>(
|
||||
extension: Extension<TConfig>,
|
||||
config?: TConfig,
|
||||
): AppNodeSpec {
|
||||
return {
|
||||
id: extension.id,
|
||||
attachTo: extension.attachTo,
|
||||
disabled: extension.disabled,
|
||||
extension,
|
||||
config,
|
||||
source: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function makeInstanceWithId<TConfig>(
|
||||
extension: Extension<TConfig>,
|
||||
config?: TConfig,
|
||||
): { id: string; instance: AppNodeInstance } {
|
||||
return {
|
||||
id: extension.id,
|
||||
instance: createAppNodeInstance({
|
||||
spec: makeSpec(extension, config),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe('instantiateAppNodeTree', () => {
|
||||
it('should instantiate a single node', () => {
|
||||
const graph = resolveAppGraph('root-node', [
|
||||
{ ...makeSpec(simpleExtension), id: 'root-node' },
|
||||
]);
|
||||
expect(graph.root.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(graph.root);
|
||||
expect(graph.root.instance).toBeDefined();
|
||||
expect(graph.root.instance?.getData(testDataRef)).toBe('test');
|
||||
|
||||
// Multiple calls should have no effect
|
||||
instantiateAppNodeTree(graph.root);
|
||||
expect(graph.root.instance).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not instantiate disabled nodes', () => {
|
||||
const graph = resolveAppGraph('root-node', [
|
||||
{ ...makeSpec(simpleExtension), id: 'root-node', disabled: true },
|
||||
]);
|
||||
expect(graph.root.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(graph.root);
|
||||
expect(graph.root.instance).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should instantiate a node with attachments', () => {
|
||||
const graph = resolveAppGraph('root-node', [
|
||||
{
|
||||
...makeSpec(
|
||||
createExtension({
|
||||
id: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ bind, inputs }) {
|
||||
bind({ inputMirror: inputs });
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
...makeSpec(simpleExtension),
|
||||
id: 'child-node',
|
||||
attachTo: { id: 'root-node', input: 'test' },
|
||||
},
|
||||
]);
|
||||
|
||||
const childNode = graph.nodes.get('child-node');
|
||||
expect(childNode).toBeDefined();
|
||||
|
||||
expect(graph.root.instance).not.toBeDefined();
|
||||
expect(childNode?.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(graph.root);
|
||||
expect(graph.root.instance).toBeDefined();
|
||||
expect(childNode?.instance).toBeDefined();
|
||||
expect(graph.root.instance?.getData(inputMirrorDataRef)).toEqual({
|
||||
test: [{ test: 'test' }],
|
||||
});
|
||||
|
||||
// Multiple calls should have no effect
|
||||
instantiateAppNodeTree(graph.root);
|
||||
expect(graph.root.instance).toBeDefined();
|
||||
expect(childNode?.instance).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not instantiate disabled attachments', () => {
|
||||
const graph = resolveAppGraph('root-node', [
|
||||
{
|
||||
...makeSpec(
|
||||
createExtension({
|
||||
id: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ bind, inputs }) {
|
||||
bind({ inputMirror: inputs });
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
...makeSpec(simpleExtension),
|
||||
id: 'child-node',
|
||||
attachTo: { id: 'root-node', input: 'test' },
|
||||
disabled: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const childNode = graph.nodes.get('child-node');
|
||||
expect(childNode).toBeDefined();
|
||||
|
||||
expect(graph.root.instance).not.toBeDefined();
|
||||
expect(childNode?.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(graph.root);
|
||||
expect(graph.root.instance).toBeDefined();
|
||||
expect(childNode?.instance).not.toBeDefined();
|
||||
expect(graph.root.instance?.getData(inputMirrorDataRef)).toEqual({
|
||||
test: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAppNodeInstance', () => {
|
||||
it('should create a simple extension instance', () => {
|
||||
const attachments = new Map();
|
||||
const instance = createAppNodeInstance({
|
||||
spec: makeSpec(simpleExtension),
|
||||
attachments,
|
||||
});
|
||||
|
||||
expect(Array.from(instance.getDataRefs())).toEqual([
|
||||
testDataRef,
|
||||
otherDataRef.optional(),
|
||||
]);
|
||||
expect(instance.getData(testDataRef)).toEqual('test');
|
||||
});
|
||||
|
||||
it('should create an extension with different kind of inputs', () => {
|
||||
const attachments = new Map([
|
||||
[
|
||||
'optionalSingletonPresent',
|
||||
[
|
||||
makeInstanceWithId(simpleExtension, {
|
||||
output: 'optionalSingletonPresent',
|
||||
}),
|
||||
],
|
||||
],
|
||||
[
|
||||
'singleton',
|
||||
[
|
||||
makeInstanceWithId(simpleExtension, {
|
||||
output: 'singleton',
|
||||
other: 2,
|
||||
}),
|
||||
],
|
||||
],
|
||||
[
|
||||
'many',
|
||||
[
|
||||
makeInstanceWithId(simpleExtension, { output: 'many1' }),
|
||||
makeInstanceWithId(simpleExtension, { output: 'many2', other: 3 }),
|
||||
],
|
||||
],
|
||||
]);
|
||||
const instance = createAppNodeInstance({
|
||||
attachments,
|
||||
spec: makeSpec(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
optionalSingletonPresent: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
optionalSingletonMissing: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
many: createExtensionInput({
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ bind, inputs }) {
|
||||
bind({ inputMirror: inputs });
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(Array.from(instance.getDataRefs())).toEqual([inputMirrorDataRef]);
|
||||
expect(instance.getData(inputMirrorDataRef)).toEqual({
|
||||
optionalSingletonPresent: { test: 'optionalSingletonPresent' },
|
||||
singleton: { test: 'singleton', other: 2 },
|
||||
many: [{ test: 'many1' }, { test: 'many2', other: 3 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should refuse to create an extension with invalid config', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
spec: {
|
||||
...makeSpec(simpleExtension),
|
||||
config: { other: 'not-a-number' },
|
||||
},
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
"Invalid configuration for extension 'core.test'; caused by Error: Expected number, received string at 'other'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should forward extension factory errors', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
spec: makeSpec(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory() {
|
||||
const error = new Error('NOPE');
|
||||
error.name = 'NopeError';
|
||||
throw error;
|
||||
},
|
||||
}),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test'; caused by NopeError: NOPE",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with duplicate output', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
spec: makeSpec(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test1: testDataRef,
|
||||
test2: testDataRef,
|
||||
},
|
||||
factory({ bind }) {
|
||||
bind({ test1: 'test', test2: 'test2' });
|
||||
},
|
||||
}),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', duplicate extension data 'test' received via output 'test2'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with disconnected output data', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
spec: makeSpec(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
},
|
||||
factory({ bind }) {
|
||||
bind({ nonexistent: 'test' } as any);
|
||||
},
|
||||
}),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', unknown output provided via 'nonexistent'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with missing required input', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
spec: makeSpec(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', input 'singleton' is required but was not received",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with undeclared inputs', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
attachments: new Map([
|
||||
[
|
||||
'declared',
|
||||
[
|
||||
makeInstanceWithId(simpleExtension, {
|
||||
output: 'many1',
|
||||
}),
|
||||
],
|
||||
],
|
||||
[
|
||||
'undeclared',
|
||||
[
|
||||
makeInstanceWithId(simpleExtension, {
|
||||
output: 'many1',
|
||||
}),
|
||||
],
|
||||
],
|
||||
]),
|
||||
spec: makeSpec(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
declared: createExtensionInput({
|
||||
test: testDataRef,
|
||||
}),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', received undeclared input 'undeclared' from extension 'core.test'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with multiple undeclared inputs', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
attachments: new Map([
|
||||
[
|
||||
'undeclared1',
|
||||
[makeInstanceWithId(simpleExtension, { output: 'many1' })],
|
||||
],
|
||||
[
|
||||
'undeclared2',
|
||||
[
|
||||
makeInstanceWithId(simpleExtension, { output: 'many1' }),
|
||||
makeInstanceWithId(simpleExtension, { output: 'many1' }),
|
||||
],
|
||||
],
|
||||
]),
|
||||
spec: makeSpec(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', received undeclared inputs 'undeclared1' from extension 'core.test' and 'undeclared2' from extensions 'core.test', 'core.test'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with multiple inputs for required singleton', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
attachments: new Map([
|
||||
[
|
||||
'singleton',
|
||||
[
|
||||
makeInstanceWithId(simpleExtension, { output: 'many1' }),
|
||||
makeInstanceWithId(simpleExtension, { output: 'many2' }),
|
||||
],
|
||||
],
|
||||
]),
|
||||
spec: makeSpec(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', expected exactly one 'singleton' input but received multiple: 'core.test', 'core.test'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with multiple inputs for optional singleton', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
attachments: new Map([
|
||||
[
|
||||
'singleton',
|
||||
[
|
||||
makeInstanceWithId(simpleExtension, { output: 'many1' }),
|
||||
makeInstanceWithId(simpleExtension, { output: 'many2' }),
|
||||
],
|
||||
],
|
||||
]),
|
||||
spec: makeSpec(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', expected at most one 'singleton' input but received multiple: 'core.test', 'core.test'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with multiple inputs that did not provide required data', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
attachments: new Map([
|
||||
['singleton', [makeInstanceWithId(simpleExtension, undefined)]],
|
||||
]),
|
||||
spec: makeSpec(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
other: otherDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', input 'singleton' did not receive required extension data 'other' from extension 'core.test'",
|
||||
);
|
||||
});
|
||||
});
|
||||
+76
-110
@@ -17,36 +17,22 @@
|
||||
import {
|
||||
AnyExtensionDataMap,
|
||||
AnyExtensionInputMap,
|
||||
BackstagePlugin,
|
||||
Extension,
|
||||
ExtensionDataRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import { AppNode, AppNodeInstance, AppNodeSpec } from './types';
|
||||
|
||||
/** @internal */
|
||||
export interface ExtensionInstance {
|
||||
readonly $$type: '@backstage/ExtensionInstance';
|
||||
|
||||
readonly id: string;
|
||||
/**
|
||||
* Get concrete value for the given extension data reference. Returns undefined if no value is available.
|
||||
*/
|
||||
getData<T>(ref: ExtensionDataRef<T>): T | undefined;
|
||||
/**
|
||||
* Maps input names to the actual instances given to them.
|
||||
*/
|
||||
readonly attachments: Map<string, ExtensionInstance[]>;
|
||||
|
||||
readonly source?: BackstagePlugin;
|
||||
}
|
||||
type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
function resolveInputData(
|
||||
dataMap: AnyExtensionDataMap,
|
||||
attachment: ExtensionInstance,
|
||||
attachment: { id: string; instance: AppNodeInstance },
|
||||
inputName: string,
|
||||
) {
|
||||
return mapValues(dataMap, ref => {
|
||||
const value = attachment.getData(ref);
|
||||
const value = attachment.instance.getData(ref);
|
||||
if (value === undefined && !ref.config.optional) {
|
||||
throw new Error(
|
||||
`input '${inputName}' did not receive required extension data '${ref.id}' from extension '${attachment.id}'`,
|
||||
@@ -58,11 +44,12 @@ function resolveInputData(
|
||||
|
||||
function resolveInputs(
|
||||
inputMap: AnyExtensionInputMap,
|
||||
attachments: Map<string, ExtensionInstance[]>,
|
||||
attachments: ReadonlyMap<string, { id: string; instance: AppNodeInstance }[]>,
|
||||
) {
|
||||
const undeclaredAttachments = Array.from(attachments.entries()).filter(
|
||||
([inputName]) => inputMap[inputName] === undefined,
|
||||
);
|
||||
// TODO: Make this a warning rather than an error
|
||||
if (undeclaredAttachments.length > 0) {
|
||||
throw new Error(
|
||||
`received undeclared input${
|
||||
@@ -79,113 +66,49 @@ function resolveInputs(
|
||||
}
|
||||
|
||||
return mapValues(inputMap, (input, inputName) => {
|
||||
const attachedInstances = attachments.get(inputName) ?? [];
|
||||
const attachedNodes = attachments.get(inputName) ?? [];
|
||||
|
||||
if (input.config.singleton) {
|
||||
if (attachedInstances.length > 1) {
|
||||
if (attachedNodes.length > 1) {
|
||||
const attachedNodeIds = attachedNodes.map(e => e.id);
|
||||
throw Error(
|
||||
`expected ${
|
||||
input.config.optional ? 'at most' : 'exactly'
|
||||
} one '${inputName}' input but received multiple: '${attachedInstances
|
||||
.map(e => e.id)
|
||||
.join("', '")}'`,
|
||||
} one '${inputName}' input but received multiple: '${attachedNodeIds.join(
|
||||
"', '",
|
||||
)}'`,
|
||||
);
|
||||
} else if (attachedInstances.length === 0) {
|
||||
} else if (attachedNodes.length === 0) {
|
||||
if (input.config.optional) {
|
||||
return undefined;
|
||||
}
|
||||
throw Error(`input '${inputName}' is required but was not received`);
|
||||
}
|
||||
return resolveInputData(
|
||||
input.extensionData,
|
||||
attachedInstances[0],
|
||||
inputName,
|
||||
);
|
||||
return resolveInputData(input.extensionData, attachedNodes[0], inputName);
|
||||
}
|
||||
|
||||
return attachedInstances.map(attachment =>
|
||||
return attachedNodes.map(attachment =>
|
||||
resolveInputData(input.extensionData, attachment, inputName),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function indent(str: string) {
|
||||
return str.replace(/^/gm, ' ');
|
||||
}
|
||||
|
||||
class ExtensionInstanceImpl implements ExtensionInstance {
|
||||
readonly $$type = '@backstage/ExtensionInstance';
|
||||
|
||||
readonly id: string;
|
||||
readonly #extensionData: Map<string, unknown>;
|
||||
readonly attachments: Map<string, ExtensionInstance[]>;
|
||||
readonly source?: BackstagePlugin;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
extensionData: Map<string, unknown>,
|
||||
attachments: Map<string, ExtensionInstance[]>,
|
||||
source: BackstagePlugin | undefined,
|
||||
) {
|
||||
this.id = id;
|
||||
this.#extensionData = extensionData;
|
||||
this.attachments = attachments;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
getData<T>(ref: ExtensionDataRef<T>): T | undefined {
|
||||
return this.#extensionData.get(ref.id) as T | undefined;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
output:
|
||||
this.#extensionData.size > 0
|
||||
? [...this.#extensionData.keys()]
|
||||
: undefined,
|
||||
attachments:
|
||||
this.attachments.size > 0
|
||||
? Object.fromEntries(this.attachments)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
toString() {
|
||||
const out =
|
||||
this.#extensionData.size > 0
|
||||
? ` out=[${[...this.#extensionData.keys()].join(', ')}]`
|
||||
: '';
|
||||
|
||||
if (this.attachments.size === 0) {
|
||||
return `<${this.id}${out} />`;
|
||||
}
|
||||
|
||||
return [
|
||||
`<${this.id}${out}>`,
|
||||
...[...this.attachments.entries()].map(([k, v]) =>
|
||||
indent([`${k} [`, ...v.map(e => indent(e.toString())), `]`].join('\n')),
|
||||
),
|
||||
`</${this.id}>`,
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createExtensionInstance(options: {
|
||||
extension: Extension<unknown>;
|
||||
config: unknown;
|
||||
source?: BackstagePlugin;
|
||||
attachments: Map<string, ExtensionInstance[]>;
|
||||
}): ExtensionInstance {
|
||||
const { extension, config, source, attachments } = options;
|
||||
export function createAppNodeInstance(options: {
|
||||
spec: AppNodeSpec;
|
||||
attachments: ReadonlyMap<string, { id: string; instance: AppNodeInstance }[]>;
|
||||
}): AppNodeInstance {
|
||||
const { spec, attachments } = options;
|
||||
const { id, extension, config, source } = spec;
|
||||
const extensionData = new Map<string, unknown>();
|
||||
const extensionDataRefs = new Set<ExtensionDataRef<unknown>>();
|
||||
|
||||
let parsedConfig: unknown;
|
||||
try {
|
||||
parsedConfig = extension.configSchema?.parse(config ?? {});
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid configuration for extension '${extension.id}'; caused by ${e}`,
|
||||
`Invalid configuration for extension '${id}'; caused by ${e}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -205,22 +128,65 @@ export function createExtensionInstance(options: {
|
||||
);
|
||||
}
|
||||
extensionData.set(ref.id, output);
|
||||
extensionDataRefs.add(ref);
|
||||
}
|
||||
},
|
||||
inputs: resolveInputs(extension.inputs, attachments),
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to instantiate extension '${extension.id}'${
|
||||
`Failed to instantiate extension '${id}'${
|
||||
e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}`
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
return new ExtensionInstanceImpl(
|
||||
options.extension.id,
|
||||
extensionData,
|
||||
attachments,
|
||||
source,
|
||||
);
|
||||
return {
|
||||
getDataRefs() {
|
||||
return extensionDataRefs.values();
|
||||
},
|
||||
getData<T>(ref: ExtensionDataRef<T>): T | undefined {
|
||||
return extensionData.get(ref.id) as T | undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting at the provided node, instantiate all reachable nodes in the graph that have not been disabled.
|
||||
* @internal
|
||||
*/
|
||||
export function instantiateAppNodeTree(rootNode: AppNode): void {
|
||||
function createInstance(node: AppNode): AppNodeInstance | undefined {
|
||||
if (node.instance) {
|
||||
return node.instance;
|
||||
}
|
||||
if (node.spec.disabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const instantiatedAttachments = new Map<
|
||||
string,
|
||||
{ id: string; instance: AppNodeInstance }[]
|
||||
>();
|
||||
|
||||
for (const [input, children] of node.edges.attachments) {
|
||||
const instantiatedChildren = children.flatMap(child => {
|
||||
const childInstance = createInstance(child);
|
||||
if (!childInstance) {
|
||||
return [];
|
||||
}
|
||||
return [{ id: child.spec.id, instance: childInstance }];
|
||||
});
|
||||
instantiatedAttachments.set(input, instantiatedChildren);
|
||||
}
|
||||
|
||||
(node as Mutable<AppNode>).instance = createAppNodeInstance({
|
||||
spec: node.spec,
|
||||
attachments: instantiatedAttachments,
|
||||
});
|
||||
|
||||
return node.instance;
|
||||
}
|
||||
|
||||
createInstance(rootNode);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user