Merge branch 'master' of github.com:backstage/backstage into port-aws-sdk

This commit is contained in:
Brian Fletcher
2023-04-13 13:17:37 +01:00
470 changed files with 12427 additions and 3011 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli-node': minor
---
Introduced the new `@backstage/cli-node` package, which provides utilities for use across Backstage CLIs.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Updates and moves OpenAPI spec to `src/schema/openapi.yaml` and uses `ApiRouter` type from `@backstage/backend-openapi-utils` to handle automatic types from the OpenAPI spec file.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Bumped create-app version.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-code-coverage-backend': patch
---
Bump `xml2js` from 0.4.23 to 0.5.0
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Fix some hard-coded white font colors in scaffolder
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Internal refactor to move many internal utilities to the new `@backstage/cli-node` package.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': minor
---
Adding two new commands to support OpenAPI spec writing, `schema:openapi:generate` to generate the Typescript file that `@backstage/backend-openapi-utils` needs for typing and `schema:openapi:verify` to verify that this file exists and matches your `src/schema/openapi.yaml` file.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-lighthouse-backend': minor
---
Lighthouse backend plugin can now use an authenticated catalog backend API.
- Breaking \* You must now pass the `tokenManager` to the lighthouse `createScheduler`.
+19
View File
@@ -0,0 +1,19 @@
---
'@backstage/plugin-catalog-react': minor
---
Attempt to load entity owner names in the EntityOwnerPicker through the `by-refs` endpoint. If `spec.profile.displayName` or `metadata.title` are populated, we now attempt to show those before showing the `humanizeEntityRef`'d version.
**BREAKING**: `EntityOwnerFilter` now uses the full entity ref instead of the `humanizeEntityRef`. If you rely on `EntityOwnerFilter.values` or the `queryParameters.owners` of `useEntityList`, you will need to adjust your code like the following.
```tsx
const { queryParameters: { owners } } = useEntityList();
// or
const { filter: { owners } } = useEntityList();
// Instead of,
if (owners.some(ref => ref === humanizeEntityRef(myEntity))) ...
// You'll need to use,
if (owners.some(ref => ref === stringifyEntityRef(myEntity))) ...
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-react': patch
---
Add an indication that the validators are running when clicking `next` on each step of the form.
+38 -3
View File
@@ -1,5 +1,5 @@
{
"mode": "pre",
"mode": "exit",
"tag": "next",
"initialVersions": {
"example-app": "0.2.81",
@@ -216,34 +216,45 @@
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.0.0",
"@backstage/plugin-search-backend-module-catalog": "0.0.0",
"@backstage/plugin-search-backend-module-explore": "0.0.0",
"@backstage/plugin-search-backend-module-techdocs": "0.0.0"
"@backstage/plugin-search-backend-module-techdocs": "0.0.0",
"@backstage/backend-openapi-utils": "0.0.0",
"@backstage/cli-node": "0.0.0"
},
"changesets": [
"afraid-horses-argue",
"all-my-auth-wanna-ride-today",
"blue-walls-drop",
"bright-hats-compare",
"bright-pumpkins-bake",
"calm-geese-obey",
"calm-otters-destroy",
"chatty-coats-thank",
"clean-dolls-search",
"clean-items-draw",
"clean-teachers-thank",
"clever-lizards-whisper",
"cold-seahorses-greet",
"create-app-1679405836",
"create-app-1681222813",
"curly-boats-trade",
"curly-jobs-kneel",
"curly-steaks-mate",
"curvy-rocks-guess",
"dependabot-ff5881d",
"eight-trainers-smoke",
"eleven-bats-tease",
"eleven-coats-refuse",
"fair-roses-stare",
"famous-beds-break",
"famous-beds-repair",
"famous-olives-try",
"fast-gorillas-approve",
"fast-snakes-buy",
"flat-ways-compete",
"forty-adults-divide",
"forty-days-tell",
"forty-gorillas-help",
"fresh-lions-compete",
"fresh-schools-fly",
"funny-pots-hide",
"funny-rivers-grin",
@@ -255,56 +266,79 @@
"hungry-lies-cry",
"hungry-monkeys-reply",
"itchy-ads-sit",
"itchy-kiwis-unite",
"khaki-cars-drum",
"khaki-doors-compare",
"khaki-guests-turn",
"late-waves-march",
"lazy-apples-destroy",
"lazy-monkeys-worry",
"long-gorillas-remain",
"many-eggs-press",
"mean-rice-beg",
"mighty-lamps-cross",
"mighty-tips-flow",
"modern-snakes-check",
"nasty-nails-appear",
"neat-donkeys-work",
"neat-pears-argue",
"new-pigs-jam",
"odd-grapes-double",
"odd-plums-bake",
"old-cougars-sit",
"olive-cycles-join",
"olive-months-talk",
"orange-rabbits-jam",
"perfect-deers-add",
"plenty-eels-listen",
"poor-cars-fold",
"poor-horses-approve",
"popular-parents-crash",
"popular-radios-visit",
"pretty-carpets-cross",
"rare-seals-decide",
"renovate-179fa0e",
"renovate-3e8a992",
"renovate-8010f99",
"renovate-a9116b7",
"renovate-c574b04",
"rotten-carrots-cheer",
"serious-items-walk",
"seven-gifts-fetch",
"shaggy-crews-carry",
"short-panthers-float",
"silent-nails-bake",
"silly-adults-accept",
"six-jobs-hear",
"sixty-insects-visit",
"slimy-scissors-play",
"slow-ravens-destroy",
"smart-crabs-dream",
"soft-roses-cover",
"sour-buttons-collect",
"sour-dragons-cheat",
"sour-laws-walk",
"spicy-carrots-argue",
"stale-cobras-beg",
"stale-files-sniff",
"stale-paws-sparkle",
"strong-crews-repeat",
"stupid-laws-play",
"sweet-eels-hunt",
"swift-meals-live",
"tall-chairs-explain",
"tall-meals-dress",
"tall-oranges-own",
"ten-hounds-bathe",
"ten-mayflies-beam",
"tender-parrots-fry",
"tender-terms-return",
"thick-forks-prove",
"thin-spoons-prove",
"thirty-crabs-tell",
"tiny-apes-scream",
"tough-cameras-beam",
"tricky-beans-knock",
"twelve-parrots-camp",
"twelve-vans-sell",
"unlucky-snakes-smile",
@@ -314,6 +348,7 @@
"wet-lamps-happen",
"wise-garlics-camp",
"witty-berries-love",
"witty-points-visit"
"witty-points-visit",
"yellow-candles-kiss"
]
}
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-react': patch
---
Updated dependency `@rjsf/core-v5` to `npm:@rjsf/core@5.5.2`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Updated dependency `@swc/helpers` to `^0.5.0`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Stripped entity types and namespace before passing to GitHub API
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-module-elasticsearch': patch
---
fix ElasticSearch throwing error when index is missing
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-org': patch
---
Added `entityLimit` to change the limit of entities displayed in the ownership boxes.
[StoryBook Example for Ownership Card](https://backstage.io/storybook/?path=/story/plugins-org-ownership-card--default)
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': patch
---
fixes a bug where an empty authorization header was provided to the proxy endpoint when a cluster had a server-side auth provider
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Reverted the check if the selected options list is different than the query parameters list before invoking `setSelectedOptions` method. This was preventing updating list items when a query string was already present in the URL when loading the page.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': minor
---
Add monorepo support to CodeOwnersProccesor.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': patch
---
`KubernetesBackendClient` now requires a `kubernetesAuthProvidersApi` value to be provided. `KubernetesApi` interface now has a proxy method requirement.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-openapi-utils': patch
---
New plugin! Primary focus is to support types on `Router`s in backend packages. Developers can use the `ApiRouter` from this package in their packages to support a typed experience based on their OpenAPI specs. The `ApiRouter` supports request bodies, response bodies, query parameters and path parameters, as well as full path-based context of the above. This means no more guessing on an endpoint like `req.post('/not-my-route', (req, res)=>{res.send(req.body.badparam)})`. Typescript would catch `/not-my-route`, `req.body.badparam`, `res.send(req.body.badparam)`.
+21
View File
@@ -0,0 +1,21 @@
---
'@backstage/core-components': minor
---
**BREAKING:** Removing `Tabs` component from `core-components` as it is neither used in the core Backstage app nor in the monorepo plugins. If you are using this component in your instance please consider replacing it with the [Material UI `Tabs`](https://v4.mui.com/components/tabs/#tabs) component like the following:
```diff
- <Tabs
- tabs={[{
- icon: <AccessAlarmIcon />,
- content: <div>Label</div>,
- }]}
- />
+ <Tabs>
+ <Tab
+ label = "Label"
+ icon = { <AccessAlarmIcon /> }
+ />
+ </Tabs>
```
+7 -7
View File
@@ -26,13 +26,13 @@ yarn.lock @backstage/maintainers @back
/plugins/azure-devops-common @backstage/maintainers @marleypowell @awanlin
/plugins/bitbucket-cloud-common @backstage/maintainers @pjungermann
/plugins/bitrise @backstage/maintainers @backstage/sda-se-reviewers
/plugins/catalog @backstage/maintainers @backstage/catalog-core
/plugins/catalog-* @backstage/maintainers @backstage/catalog-core
/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/catalog-core @pjungermann
/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/catalog-core @pjungermann
/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/catalog-core @pjungermann
/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/catalog-core @tdabasinskas
/plugins/catalog-graph @backstage/maintainers @backstage/catalog-core @backstage/sda-se-reviewers
/plugins/catalog @backstage/maintainers @backstage/catalog-maintainers
/plugins/catalog-* @backstage/maintainers @backstage/catalog-maintainers
/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/catalog-maintainers @pjungermann
/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/catalog-maintainers @pjungermann
/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/catalog-maintainers @pjungermann
/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/catalog-maintainers @tdabasinskas
/plugins/catalog-graph @backstage/maintainers @backstage/catalog-maintainers @backstage/sda-se-reviewers
/plugins/circleci @backstage/maintainers @adamdmharvey
/plugins/cloudbuild @backstage/maintainers @trivago/ebarrios
/plugins/code-coverage @backstage/maintainers @alde @nissayeva
+3
View File
@@ -92,6 +92,9 @@ jobs:
- name: verify api reference
run: node scripts/verify-api-reference.js
- name: verify openapi yaml file matches generated ts file
run: yarn backstage-repo-tools schema:openapi:verify
- name: verify doc links
run: node scripts/verify-links.js
+8 -1
View File
@@ -14,6 +14,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
git-ref: ${{ env.GIT_REF }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
@@ -67,10 +68,15 @@ jobs:
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: Read Git Ref From Event Object
id: ref
run: echo "GIT_REF=${{ fromJSON(env.EVENT_JSON).head.sha }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Git Ref: ${{ env.GIT_REF }}"
echo "Compose file hash: ${{ env.COMPOSE_FILE_HASH }}"
cat event.json
@@ -85,8 +91,9 @@ jobs:
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com
git-ref: ${{ needs.cache-compose-file.outputs.git-ref }}
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
server: https://app.uffizzi.com
permissions:
contents: read
pull-requests: write
+4
View File
@@ -237,3 +237,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [Comcast](https://comcast.github.io/) | [Ryan Emerle](https://github.com/remerle) | Developer portal enabling discovery of products, services, and documentation throughout the enterprise to ultimately reduce friction and improve time-to-market. |
| [Syntasso](https://www.syntasso.io/) | [@syntassodev](https://github.com/syntassodev) | Backstage is used as a optional UI for the [Kratix project](https://kratix.io), a framework for building platforms.
| [Bankdata](https://www.bankdata.dk) | [Thomas Grønbæk](https://www.linkedin.com/in/thomas-grønbæk/) | We are assessing Backstage as our internal developer portal @Bankdata. Goal is to reduce cognitive load and friction for developers and thereby improving developer experience, onboarding and productivity. Expect use of Software Catalog for discoverability and ownership, Tech Docs and Software Templates to integrate tooling and best practices.
| [B3](https://www.b3.com.br/) | [Marcos Rodrigues](https://www.linkedin.com/in/marcos-rodrigues-cloud/) | B3 (Brazilian Stock Exchange) will implement a self-service platform focused on software development based on Backstage. The tool will primarily focus on product development and software engineering teams, as well as professionals from other areas who are also involved in software delivery. The new platform will function as a portal where teams can access tools, codes, and templates that have already been tested and are available, serving as building blocks for the development of new solutions. |
| [Porto](https://www.portoseguro.com.br/) | [Camilo Alessandro](https://www.linkedin.com/in/camilo-alessandro/) | Porto (Brazilian insurance company) Centralized developer portal with software catalog, application templates, maturity radar, component taxonomy standardization, and automation in the integration with DevSecOps processes. |
| [Einride](https://github.com/einride) | [@odsod](https://github.com/odsod/) | We use Backstage to create a great developer experience across Einride's entire software stack - from autonomous and electric vehicles to cloud systems - and we've developed a [Backstage Go SDK](https://github.com/einride/backstage-go) for interfacing with Backstage from our Go tooling.
| [Chartboost](https://www.chartboost.com)| [@brucearctor](https://github.com/brucearctor), [@ArtemChekunov](https://github.com/ArtemChekunov) | We are building our internal develper portal on top of Backstage.|
+9 -3
View File
@@ -20,9 +20,14 @@ These are the separate project areas of Backstage, each with their own project a
Team: @backstage/catalog-maintainers
| Name | Organization | Team | GitHub | Discord |
| ---- | ------------ | --------- | ---------------------------- | ------- |
| TBD | Spotify | Chipmunks | [TBD](http://github.com/TBD) | - |
Scope: The catalog plugin and catalog model
| Name | Organization | Team | GitHub | Discord |
| -------------- | ------------ | --------- | ---------------------------------------- | ---------------- |
| Rickard Dybeck | Spotify | Chipmunks | [alde](http://github.com/alde) | rdybeck#8083 |
| Mike Blockley | Spotify | Chipmunks | [mikeyhc](http://github.com/mikeyhc) | mikey-spot#5363 |
| Elon Jefferson | Spotify | Chipmunks | [Edje-C](http://github.com/Edje-C) | elon-spotty#6086 |
| Nurit Izrailov | Spotify | Chipmunks | [nuritizra](http://github.com/nuritizra) | - |
### Discoverability
@@ -91,6 +96,7 @@ Scope: The TechDocs plugin and related tooling
| Carlos Esteban Lopez Jaramillo | VMWare | [luchillo17](https://github.com/luchillo17) | `luchillo17#8777` |
| David Tuite | Roadie | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` |
| Himanshu Mishra | Harness.io | [OrkoHunter](https://github.com/OrkoHunter) | `OrkoHunter#1520` |
| Irma Solakovic | Roadie | [Irma12](https://github.com/Irma12) | `Irma#7629` |
| Jamie Klassen | VMware | [jamieklassen](https://github.com/jamieklassen) | `jamieklassen#3047` |
| Jussi Hallila | Roadie | [Xantier](https://github.com/Xantier) | `Xantier#0086` |
| Mark Avery | Cvent | [webark](https://github.com/webark) | `webark#8471` |
+5 -31
View File
@@ -14,42 +14,16 @@ Kubernetes backend plugin's proxy endpoint to allow them to make arbitrary
requests to the [REST
API](https://kubernetes.io/docs/reference/using-api/api-concepts/).
Here is a snippet fetching namespaces from a cluster configured with the
`google` [auth provider](https://backstage.io/docs/features/kubernetes/configuration#clustersauthprovider):
Here is a snippet fetching namespaces using the `KubernetesBackendClient` library
```typescript
import {
discoveryApiRef,
googleAuthApiRef,
useApi,
identityApiRef,
} from '@backstage/core-plugin-api';
import { useApi } from '@backstage/core-plugin-api';
import { kubernetesApiRef } from '@backstage/plugin-kubernetes';
const CLUSTER_NAME = ''; // use a known cluster name
// get a bearer token from Google
const googleAuthApi = useApi(googleAuthApiRef);
const token = await googleAuthApi.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform',
);
// get a backstage ID token
const identityApi = useApi(identityApiRef);
const { token: userToken } = await identityApi.getCredentials();
const discoveryApi = useApi(discoveryApiRef);
const kubernetesBaseUrl = await discoveryApi.getBaseUrl('kubernetes');
const kubernetesProxyEndpoint = `${kubernetesBaseUrl}/proxy`;
// fetch namespaces
await fetch(`${kubernetesProxyEndpoint}/api/v1/namespaces`, {
method: 'GET',
headers: {
'Backstage-Kubernetes-Cluster': CLUSTER_NAME,
'Backstage-Kubernetes-Authorization': `Bearer ${token}`,
Authorization: `Bearer ${userToken}`,
},
});
const kubernetesApi = useApi(kubernetesApiRef);
await kubernetesApi.proxy(CLUSTER_NAME, '/api/v1/namespaces');
```
## How it works
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -32,7 +32,7 @@
"@docusaurus/core": "2.4.0",
"@docusaurus/plugin-client-redirects": "^2.3.1",
"@docusaurus/preset-classic": "2.4.0",
"@swc/core": "^1.3.36",
"@swc/core": "^1.3.46",
"clsx": "^1.1.1",
"docusaurus-plugin-sass": "^0.2.3",
"prism-react-renderer": "^1.3.5",
+47 -45
View File
@@ -2624,90 +2624,92 @@ __metadata:
languageName: node
linkType: hard
"@swc/core-darwin-arm64@npm:1.3.42":
version: 1.3.42
resolution: "@swc/core-darwin-arm64@npm:1.3.42"
"@swc/core-darwin-arm64@npm:1.3.46":
version: 1.3.46
resolution: "@swc/core-darwin-arm64@npm:1.3.46"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@swc/core-darwin-x64@npm:1.3.42":
version: 1.3.42
resolution: "@swc/core-darwin-x64@npm:1.3.42"
"@swc/core-darwin-x64@npm:1.3.46":
version: 1.3.46
resolution: "@swc/core-darwin-x64@npm:1.3.46"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@swc/core-linux-arm-gnueabihf@npm:1.3.42":
version: 1.3.42
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.42"
"@swc/core-linux-arm-gnueabihf@npm:1.3.46":
version: 1.3.46
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.46"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@swc/core-linux-arm64-gnu@npm:1.3.42":
version: 1.3.42
resolution: "@swc/core-linux-arm64-gnu@npm:1.3.42"
"@swc/core-linux-arm64-gnu@npm:1.3.46":
version: 1.3.46
resolution: "@swc/core-linux-arm64-gnu@npm:1.3.46"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@swc/core-linux-arm64-musl@npm:1.3.42":
version: 1.3.42
resolution: "@swc/core-linux-arm64-musl@npm:1.3.42"
"@swc/core-linux-arm64-musl@npm:1.3.46":
version: 1.3.46
resolution: "@swc/core-linux-arm64-musl@npm:1.3.46"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@swc/core-linux-x64-gnu@npm:1.3.42":
version: 1.3.42
resolution: "@swc/core-linux-x64-gnu@npm:1.3.42"
"@swc/core-linux-x64-gnu@npm:1.3.46":
version: 1.3.46
resolution: "@swc/core-linux-x64-gnu@npm:1.3.46"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@swc/core-linux-x64-musl@npm:1.3.42":
version: 1.3.42
resolution: "@swc/core-linux-x64-musl@npm:1.3.42"
"@swc/core-linux-x64-musl@npm:1.3.46":
version: 1.3.46
resolution: "@swc/core-linux-x64-musl@npm:1.3.46"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@swc/core-win32-arm64-msvc@npm:1.3.42":
version: 1.3.42
resolution: "@swc/core-win32-arm64-msvc@npm:1.3.42"
"@swc/core-win32-arm64-msvc@npm:1.3.46":
version: 1.3.46
resolution: "@swc/core-win32-arm64-msvc@npm:1.3.46"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@swc/core-win32-ia32-msvc@npm:1.3.42":
version: 1.3.42
resolution: "@swc/core-win32-ia32-msvc@npm:1.3.42"
"@swc/core-win32-ia32-msvc@npm:1.3.46":
version: 1.3.46
resolution: "@swc/core-win32-ia32-msvc@npm:1.3.46"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@swc/core-win32-x64-msvc@npm:1.3.42":
version: 1.3.42
resolution: "@swc/core-win32-x64-msvc@npm:1.3.42"
"@swc/core-win32-x64-msvc@npm:1.3.46":
version: 1.3.46
resolution: "@swc/core-win32-x64-msvc@npm:1.3.46"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@swc/core@npm:^1.3.36":
version: 1.3.42
resolution: "@swc/core@npm:1.3.42"
"@swc/core@npm:^1.3.46":
version: 1.3.46
resolution: "@swc/core@npm:1.3.46"
dependencies:
"@swc/core-darwin-arm64": 1.3.42
"@swc/core-darwin-x64": 1.3.42
"@swc/core-linux-arm-gnueabihf": 1.3.42
"@swc/core-linux-arm64-gnu": 1.3.42
"@swc/core-linux-arm64-musl": 1.3.42
"@swc/core-linux-x64-gnu": 1.3.42
"@swc/core-linux-x64-musl": 1.3.42
"@swc/core-win32-arm64-msvc": 1.3.42
"@swc/core-win32-ia32-msvc": 1.3.42
"@swc/core-win32-x64-msvc": 1.3.42
"@swc/core-darwin-arm64": 1.3.46
"@swc/core-darwin-x64": 1.3.46
"@swc/core-linux-arm-gnueabihf": 1.3.46
"@swc/core-linux-arm64-gnu": 1.3.46
"@swc/core-linux-arm64-musl": 1.3.46
"@swc/core-linux-x64-gnu": 1.3.46
"@swc/core-linux-x64-musl": 1.3.46
"@swc/core-win32-arm64-msvc": 1.3.46
"@swc/core-win32-ia32-msvc": 1.3.46
"@swc/core-win32-x64-msvc": 1.3.46
peerDependencies:
"@swc/helpers": ^0.5.0
dependenciesMeta:
"@swc/core-darwin-arm64":
optional: true
@@ -2729,7 +2731,7 @@ __metadata:
optional: true
"@swc/core-win32-x64-msvc":
optional: true
checksum: 5c28c03ee6a4e1f630c4a519b56d7e23a0102f70fad977a3ab144a3da96231cf4849e473cdc9ee7be6b9266ba0838d0bc0b6b3575a75540f302befe96cc9606e
checksum: bd1773bf9e7940b540d4f7a36bf109594562fcca338ec60d5a95bd1bc9fc2e3e632351c3c9cf803f6947bf7ebc200e5260d034c49db8d28689d96d031e2836b1
languageName: node
linkType: hard
@@ -3732,7 +3734,7 @@ __metadata:
"@docusaurus/plugin-client-redirects": ^2.3.1
"@docusaurus/preset-classic": 2.4.0
"@spotify/prettier-config": ^14.0.0
"@swc/core": ^1.3.36
"@swc/core": ^1.3.46
"@tsconfig/docusaurus": ^1.0.6
"@types/webpack-env": ^1.18.0
clsx: ^1.1.1
+1 -1
View File
@@ -47,7 +47,7 @@
"@types/react": "^17",
"@types/react-dom": "^17"
},
"version": "1.13.0-next.2",
"version": "1.13.0-next.3",
"dependencies": {
"@backstage/errors": "workspace:^",
"@manypkg/get-packages": "^1.1.3"
+11
View File
@@ -1,5 +1,16 @@
# @backstage/app-defaults
## 1.3.0-next.3
### Patch Changes
- Updated dependencies
- @backstage/core-app-api@1.7.0-next.3
- @backstage/core-components@0.13.0-next.3
- @backstage/core-plugin-api@1.5.1-next.1
- @backstage/theme@0.2.19-next.0
- @backstage/plugin-permission-react@0.4.12-next.1
## 1.3.0-next.2
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/app-defaults",
"description": "Provides the default wiring of a Backstage App",
"version": "1.3.0-next.2",
"version": "1.3.0-next.3",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
+69
View File
@@ -1,5 +1,74 @@
# example-app
## 0.2.82-next.3
### Patch Changes
- Updated dependencies
- @backstage/plugin-kubernetes@0.8.0-next.3
- @backstage/plugin-scaffolder@1.13.0-next.3
- @backstage/cli@0.22.6-next.3
- @backstage/plugin-explore@0.4.2-next.3
- @backstage/plugin-catalog-react@1.5.0-next.3
- @backstage/plugin-scaffolder-react@1.3.0-next.3
- @backstage/core-app-api@1.7.0-next.3
- @backstage/catalog-model@1.3.0-next.0
- @backstage/plugin-techdocs@1.6.1-next.3
- @backstage/core-components@0.13.0-next.3
- @backstage/app-defaults@1.3.0-next.3
- @backstage/config@1.0.7
- @backstage/core-plugin-api@1.5.1-next.1
- @backstage/integration-react@1.1.12-next.3
- @backstage/theme@0.2.19-next.0
- @backstage/plugin-airbrake@0.3.17-next.3
- @backstage/plugin-apache-airflow@0.2.10-next.3
- @backstage/plugin-api-docs@0.9.2-next.3
- @backstage/plugin-azure-devops@0.2.8-next.3
- @backstage/plugin-azure-sites@0.1.6-next.3
- @backstage/plugin-badges@0.2.41-next.3
- @backstage/plugin-catalog-common@1.0.13-next.1
- @backstage/plugin-catalog-graph@0.2.29-next.3
- @backstage/plugin-catalog-import@0.9.7-next.3
- @backstage/plugin-circleci@0.3.17-next.3
- @backstage/plugin-cloudbuild@0.3.17-next.3
- @backstage/plugin-code-coverage@0.2.10-next.3
- @backstage/plugin-cost-insights@0.12.6-next.3
- @backstage/plugin-dynatrace@4.0.0-next.3
- @backstage/plugin-entity-feedback@0.2.0-next.3
- @backstage/plugin-gcalendar@0.3.13-next.3
- @backstage/plugin-gcp-projects@0.3.36-next.3
- @backstage/plugin-github-actions@0.5.17-next.3
- @backstage/plugin-gocd@0.1.23-next.3
- @backstage/plugin-graphiql@0.2.49-next.3
- @backstage/plugin-home@0.4.33-next.3
- @backstage/plugin-jenkins@0.7.16-next.3
- @backstage/plugin-kafka@0.3.17-next.3
- @backstage/plugin-lighthouse@0.4.2-next.3
- @backstage/plugin-linguist@0.1.2-next.3
- @backstage/plugin-linguist-common@0.1.0
- @backstage/plugin-microsoft-calendar@0.1.2-next.3
- @backstage/plugin-newrelic@0.3.35-next.3
- @backstage/plugin-newrelic-dashboard@0.2.10-next.3
- @backstage/plugin-org@0.6.7-next.3
- @backstage/plugin-pagerduty@0.5.10-next.3
- @backstage/plugin-permission-react@0.4.12-next.1
- @backstage/plugin-playlist@0.1.8-next.3
- @backstage/plugin-rollbar@0.4.17-next.3
- @backstage/plugin-search@1.2.0-next.3
- @backstage/plugin-search-common@1.2.3-next.0
- @backstage/plugin-search-react@1.5.2-next.3
- @backstage/plugin-sentry@0.5.2-next.3
- @backstage/plugin-shortcuts@0.3.9-next.3
- @backstage/plugin-stack-overflow@0.1.13-next.3
- @backstage/plugin-stackstorm@0.1.1-next.3
- @backstage/plugin-tech-insights@0.3.9-next.3
- @backstage/plugin-tech-radar@0.6.3-next.3
- @backstage/plugin-techdocs-module-addons-contrib@1.0.12-next.3
- @backstage/plugin-techdocs-react@1.1.5-next.3
- @backstage/plugin-todo@0.2.19-next.3
- @backstage/plugin-user-settings@0.7.2-next.3
- @internal/plugin-catalog-customized@0.0.9-next.3
## 0.2.82-next.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-app",
"version": "0.2.82-next.2",
"version": "0.2.82-next.3",
"private": true,
"backstage": {
"role": "frontend"
+22
View File
@@ -1,5 +1,27 @@
# example-backend-next
## 0.0.10-next.3
### Patch Changes
- Updated dependencies
- @backstage/plugin-kubernetes-backend@0.10.0-next.3
- @backstage/plugin-catalog-backend@1.9.0-next.3
- @backstage/plugin-scaffolder-backend@1.13.0-next.3
- @backstage/backend-defaults@0.1.9-next.2
- @backstage/plugin-app-backend@0.3.44-next.2
- @backstage/plugin-auth-node@0.2.13-next.2
- @backstage/plugin-permission-backend@0.5.19-next.2
- @backstage/plugin-permission-common@0.7.5-next.0
- @backstage/plugin-permission-node@0.7.7-next.2
- @backstage/plugin-search-backend@1.3.0-next.2
- @backstage/plugin-search-backend-module-catalog@0.1.0-next.2
- @backstage/plugin-search-backend-module-explore@0.1.0-next.1
- @backstage/plugin-search-backend-module-techdocs@0.1.0-next.2
- @backstage/plugin-search-backend-node@1.2.0-next.2
- @backstage/plugin-techdocs-backend@1.6.1-next.3
- @backstage/plugin-todo-backend@0.1.41-next.3
## 0.0.10-next.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend-next",
"version": "0.0.10-next.2",
"version": "0.0.10-next.3",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,7 @@
# @backstage/backend-openapi-utils
## 0.0.1-next.0
### Patch Changes
- 62fe726fdc5: New plugin! Primary focus is to support types on `Router`s in backend packages. Developers can use the `ApiRouter` from this package in their packages to support a typed experience based on their OpenAPI specs. The `ApiRouter` supports request bodies, response bodies, query parameters and path parameters, as well as full path-based context of the above. This means no more guessing on an endpoint like `req.post('/not-my-route', (req, res)=>{res.send(req.body.badparam)})`. Typescript would catch `/not-my-route`, `req.body.badparam`, `res.send(req.body.badparam)`.
+45
View File
@@ -0,0 +1,45 @@
# @backstage/openapi-utils
## Summary
This package is meant to provide a typed Express router for an OpenAPI spec. Based on the [`oatx`](https://github.com/varanauskas/oatx) library and adapted to override Express values.
## Getting Started
### Configuration
1. Run `yarn --cwd <package-dir> backstage-cli package schema:openapi:generate` to translate your `src/schema/openapi.yaml` to a new Typescript file in `src/schema/openapi.ts`. In the case of projects that require linting + a license header, you will need to do this manually.
2. In your plugin's `src/service/createRouter.ts`,
```ts
import {ApiRouter} from `@backstage/backend-openapi-utils`;
import spec from '../schema/openapi'
...
export function createRouter(){
const router = Router() as ApiRouter<typeof spec>;
...
}
```
### Limitations
1. `as const` makes all fields `readonly`
To ensure a good DX of using a simple imported JSON spec, we want to remove any type issues between `readonly` arrays and mutable arrays. Typescript does not allow them to be compared, so converting all imports from the `openapi3-ts` library to `readonly` is important. This is achieved through the `ImmutableObject` type in `types/immutable.ts`.
```ts
...
// We want an interface like this,
Router() as ApiRouter<typeof spec>
// Not an interface like this,
Router() as ApiRouter<DeepWriteable<typeof spec>>
...
```
## Future Work
### Runtime validation
Using a package like [`express-openapi-validator`](https://www.npmjs.com/package/express-openapi-validator), would allow us to remove [validation of request bodies with `AJV`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/util.ts#L58). However, `AJV` currently doesn't have support for OpenAPI 3.1 and `express-openapi-validator` enforces full URL matching for paths, meaning it cannot be mounted at the router level.
@@ -0,0 +1,644 @@
## API Report File for "@backstage/backend-openapi-utils"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import type { ContentObject } from 'openapi3-ts';
import type core from 'express-serve-static-core';
import { FromSchema } from 'json-schema-to-ts';
import { JSONSchema7 } from 'json-schema-to-ts';
import type { OpenAPIObject } from 'openapi3-ts';
import type { ParameterObject } from 'openapi3-ts';
import type { ReferenceObject } from 'openapi3-ts';
import type { RequestBodyObject } from 'openapi3-ts';
import type { ResponseObject } from 'openapi3-ts';
import { Router } from 'express';
import type { SchemaObject } from 'openapi3-ts';
// @public
export interface ApiRouter<Doc extends RequiredDoc> extends Router {
// (undocumented)
all: DocRequestMatcher<Doc, this, 'all'>;
// (undocumented)
delete: DocRequestMatcher<Doc, this, 'delete'>;
// (undocumented)
get: DocRequestMatcher<Doc, this, 'get'>;
// (undocumented)
head: DocRequestMatcher<Doc, this, 'head'>;
// (undocumented)
options: DocRequestMatcher<Doc, this, 'options'>;
// (undocumented)
patch: DocRequestMatcher<Doc, this, 'patch'>;
// (undocumented)
post: DocRequestMatcher<Doc, this, 'post'>;
// (undocumented)
put: DocRequestMatcher<Doc, this, 'put'>;
}
// @public (undocumented)
type ComponentRef<
Doc extends RequiredDoc,
Type extends ComponentTypes<Doc>,
Ref extends ImmutableReferenceObject,
> = Ref extends {
$ref: `#/components/${Type}/${infer Name}`;
}
? Name extends keyof Doc['components'][Type]
? Doc['components'][Type][Name] extends ImmutableReferenceObject
? ComponentRef<Doc, Type, Doc['components'][Type][Name]>
: Doc['components'][Type][Name]
: never
: never;
// @public (undocumented)
type ComponentTypes<Doc extends RequiredDoc> = Extract<
keyof Doc['components'],
string
>;
// @public (undocumented)
type ConvertAll<T, R extends ReadonlyArray<unknown> = []> = T extends [
infer First extends JSONSchema7,
...infer Rest,
]
? ConvertAll<Rest, [...R, FromSchema<First>]>
: R;
// @public (undocumented)
interface CookieObject extends ParameterObject {
// (undocumented)
in: 'cookie';
// (undocumented)
style?: 'form';
}
// @public (undocumented)
type CookieSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableCookieObject>;
// @public (undocumented)
type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = Extract<
T,
Record<K, V>
>;
// @public (undocumented)
type DocOperation<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
> = Doc['paths'][Path][Method];
// @public (undocumented)
type DocParameter<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
Parameter extends keyof Doc['paths'][Path][Method]['parameters'],
> = DocOperation<
Doc,
Path,
Method
>['parameters'][Parameter] extends ImmutableReferenceObject
? 'parameters' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'parameters',
DocOperation<Doc, Path, Method>['parameters'][Parameter]
>
: never
: DocOperation<Doc, Path, Method>['parameters'][Parameter];
// @public (undocumented)
type DocParameters<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
> = DocOperation<Doc, Path, Method>['parameters'] extends ReadonlyArray<any>
? {
[Index in keyof DocOperation<
Doc,
Path,
Method
>['parameters']]: DocParameter<Doc, Path, Method, Index>;
}
: never;
// @public
type DocPath<
Doc extends PathDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
> = ValueOf<{
[Template in Extract<
keyof Doc['paths'],
string
>]: Path extends PathTemplate<Template> ? Template : never;
}>;
// @public (undocumented)
type DocPathMethod<
Doc extends Pick<RequiredDoc, 'paths'>,
Path extends DocPathTemplate<Doc>,
> = keyof Doc['paths'][DocPath<Doc, Path>];
// @public (undocumented)
type DocPathTemplate<Doc extends PathDoc> = PathTemplate<
Extract<keyof Doc['paths'], string>
>;
// @public
type DocRequestHandler<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends keyof Doc['paths'][Path],
> = core.RequestHandler<
PathSchema<Doc, Path, Method>,
ResponseBodyToJsonSchema<Doc, Path, Method>,
RequestBodyToJsonSchema<Doc, Path, Method>,
QuerySchema<Doc, Path, Method>,
Record<string, string>
>;
// @public
type DocRequestHandlerParams<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends keyof Doc['paths'][Path],
> = core.RequestHandlerParams<
PathSchema<Doc, Path, Method>,
ResponseBodyToJsonSchema<Doc, Path, Method>,
RequestBodyToJsonSchema<Doc, Path, Method>,
QuerySchema<Doc, Path, Method>,
Record<string, string>
>;
// @public
interface DocRequestMatcher<
Doc extends RequiredDoc,
T,
Method extends
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head',
> {
// (undocumented)
<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, Method>>(
path: Path,
...handlers: Array<DocRequestHandler<Doc, Path, Method>>
): T;
// (undocumented)
<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, Method>>(
path: Path,
...handlers: Array<DocRequestHandlerParams<Doc, Path, Method>>
): T;
}
// @public (undocumented)
type Filter<T, U> = T extends U ? T : never;
// @public (undocumented)
type FullMap<
T extends {
[key: string]: any;
},
> = RequiredMap<T> & OptionalMap<T>;
// @public (undocumented)
interface HeaderObject extends ParameterObject {
// (undocumented)
in: 'header';
// (undocumented)
style: 'simple';
}
// @public (undocumented)
type HeaderSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableHeaderObject>;
// @public
type Immutable<T> = T extends
| Function
| boolean
| number
| string
| null
| undefined
? T
: T extends Map<infer K, infer V>
? ReadonlyMap<Immutable<K>, Immutable<V>>
: T extends Set<infer S>
? ReadonlySet<Immutable<S>>
: {
readonly [P in keyof T]: Immutable<T[P]>;
};
// @public (undocumented)
type ImmutableContentObject = ImmutableObject<ContentObject>;
// @public (undocumented)
type ImmutableCookieObject = ImmutableObject<CookieObject>;
// @public (undocumented)
type ImmutableHeaderObject = ImmutableObject<HeaderObject>;
// @public (undocumented)
type ImmutableObject<T> = {
readonly [K in keyof T]: Immutable<T[K]>;
};
// @public (undocumented)
type ImmutableOpenAPIObject = ImmutableObject<OpenAPIObject>;
// @public (undocumented)
type ImmutableParameterObject = ImmutableObject<ParameterObject>;
// @public (undocumented)
type ImmutablePathObject = ImmutableObject<PathObject>;
// @public (undocumented)
type ImmutableQueryObject = ImmutableObject<QueryObject>;
// @public (undocumented)
type ImmutableReferenceObject = ImmutableObject<ReferenceObject>;
// @public (undocumented)
type ImmutableRequestBodyObject = ImmutableObject<RequestBodyObject>;
// @public (undocumented)
type ImmutableResponseObject = ImmutableObject<ResponseObject>;
// @public (undocumented)
type ImmutableSchemaObject = ImmutableObject<SchemaObject>;
declare namespace internal {
export {
RequiredDoc,
PathDoc,
ValueOf,
PathTemplate,
DocPath,
DocPathTemplate,
DocPathMethod,
MethodAwareDocPath,
DocOperation,
ComponentTypes,
ComponentRef,
SchemaRef,
ObjectWithContentSchema,
UnionToIntersection,
LastOf,
Push,
TuplifyUnion,
ConvertAll,
UnknownIfNever,
ToTypeSafe,
DiscriminateUnion,
MapDiscriminatedUnion,
PickOptionalKeys,
PickRequiredKeys,
OptionalMap,
RequiredMap,
FullMap,
Filter,
DocRequestHandler,
DocRequestHandlerParams,
DocRequestMatcher,
Immutable,
ImmutableObject,
ImmutableReferenceObject,
ImmutableOpenAPIObject,
ImmutableContentObject,
ImmutableRequestBodyObject,
ImmutableResponseObject,
ImmutableParameterObject,
HeaderObject,
ImmutableHeaderObject,
CookieObject,
ImmutableCookieObject,
QueryObject,
ImmutableQueryObject,
PathObject,
ImmutablePathObject,
ImmutableSchemaObject,
DocParameter,
DocParameters,
ResolveDocParameterSchema,
ParameterSchema,
MapToSchema,
ParametersSchema,
HeaderSchema,
CookieSchema,
PathSchema,
QuerySchema,
RequestBody,
RequestBodySchema,
RequestBodyToJsonSchema,
Response_2 as Response,
ResponseSchemas,
ResponseBodyToJsonSchema,
};
}
export { internal };
// @public (undocumented)
type LastOf<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer R
? R
: never;
// @public (undocumented)
type MapDiscriminatedUnion<T extends Record<K, string>, K extends keyof T> = {
[V in T[K]]: DiscriminateUnion<T, K, V>;
};
// @public (undocumented)
type MapToSchema<
Doc extends RequiredDoc,
T extends Record<string, ImmutableParameterObject>,
> = {
[V in keyof T]: NonNullable<T[V]> extends ImmutableParameterObject
? ParameterSchema<Doc, NonNullable<T[V]>['schema']>
: never;
};
// @public (undocumented)
type MethodAwareDocPath<
Doc extends PathDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends keyof Doc['paths'][Path],
> = ValueOf<{
[Template in Extract<
keyof Doc['paths'],
string
>]: Path extends PathTemplate<Template>
? Method extends DocPathMethod<Doc, Path>
? PathTemplate<Template>
: never
: never;
}>;
// @public (undocumented)
type ObjectWithContentSchema<
Doc extends RequiredDoc,
Object extends {
content?: ImmutableContentObject;
},
> = Object['content'] extends ImmutableContentObject
? SchemaRef<Doc, Object['content']['application/json']['schema']>
: never;
// @public (undocumented)
type OptionalMap<
T extends {
[key: string]: any;
},
> = {
[P in Exclude<PickOptionalKeys<T>, undefined>]?: NonNullable<T[P]>;
};
// @public (undocumented)
type ParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
> = ResolveDocParameterSchema<Doc, Schema> extends infer R
? R extends ImmutableSchemaObject
? R extends JSONSchema7
? FromSchema<R>
: never
: never
: never;
// @public (undocumented)
type ParametersSchema<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
FilterType extends ImmutableParameterObject,
> = number extends keyof DocParameters<Doc, Path, Method>
? MapToSchema<
Doc,
FullMap<
MapDiscriminatedUnion<
Filter<DocParameters<Doc, Path, Method>[number], FilterType>,
'name'
>
>
>
: never;
// @public (undocumented)
type PathDoc = Pick<ImmutableOpenAPIObject, 'paths'>;
// @public (undocumented)
interface PathObject extends ParameterObject {
// (undocumented)
in: 'path';
// (undocumented)
style?: 'simple' | 'label' | 'matrix';
}
// @public (undocumented)
type PathSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutablePathObject>;
// @public
type PathTemplate<Path extends string> =
Path extends `${infer Prefix}{${infer PathName}}${infer Suffix}`
? `${Prefix}:${PathName}${PathTemplate<Suffix>}`
: Path;
// @public (undocumented)
type PickOptionalKeys<
T extends {
[key: string]: any;
},
> = {
[K in keyof T]: true extends T[K]['required'] ? never : K;
}[keyof T];
// @public (undocumented)
type PickRequiredKeys<
T extends {
[key: string]: any;
},
> = {
[K in keyof T]: true extends T[K]['required'] ? K : never;
}[keyof T];
// @public (undocumented)
type Push<T extends any[], V> = [...T, V];
// @public (undocumented)
interface QueryObject extends ParameterObject {
// (undocumented)
in: 'query';
// (undocumented)
style?: 'form' | 'deepObject' | 'pipeDelimited' | 'spaceDelimited';
}
// @public (undocumented)
type QuerySchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableQueryObject>;
// @public (undocumented)
type RequestBody<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
> = DocOperation<
Doc,
Path,
Method
>['requestBody'] extends ImmutableReferenceObject
? 'requestBodies' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'requestBodies',
DocOperation<Doc, Path, Method>['requestBody']
>
: never
: DocOperation<Doc, Path, Method>['requestBody'];
// @public (undocumented)
type RequestBodySchema<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = RequestBody<
Doc,
DocPath<Doc, Path>,
Method
> extends ImmutableRequestBodyObject
? ObjectWithContentSchema<Doc, RequestBody<Doc, DocPath<Doc, Path>, Method>>
: never;
// @public
type RequestBodyToJsonSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ToTypeSafe<RequestBodySchema<Doc, Path, Method>>;
// @public
type RequiredDoc = Pick<ImmutableOpenAPIObject, 'paths' | 'components'>;
// @public (undocumented)
type RequiredMap<
T extends {
[key: string]: any;
},
> = {
[P in Exclude<PickRequiredKeys<T>, undefined>]: NonNullable<T[P]>;
};
// @public (undocumented)
type ResolveDocParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
> = Schema extends ImmutableReferenceObject
? 'parameters' extends ComponentTypes<Doc>
? ComponentRef<Doc, 'parameters', Schema>
: never
: Schema;
// @public (undocumented)
type Response_2<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
StatusCode extends keyof DocOperation<Doc, Path, Method>['responses'],
> = DocOperation<
Doc,
Path,
Method
>['responses'][StatusCode] extends ImmutableReferenceObject
? 'responses' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'responses',
DocOperation<Doc, Path, Method>['responses'][StatusCode]
>
: never
: DocOperation<Doc, Path, Method>['responses'][StatusCode];
// @public
type ResponseBodyToJsonSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ToTypeSafe<ValueOf<ResponseSchemas<Doc, Path, Method>>>;
// @public (undocumented)
type ResponseSchemas<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = {
[StatusCode in keyof DocOperation<
Doc,
Path,
Method
>['responses']]: Response_2<
Doc,
Path,
Method,
StatusCode
> extends ImmutableResponseObject
? ObjectWithContentSchema<Doc, Response_2<Doc, Path, Method, StatusCode>>
: never;
};
// @public (undocumented)
type SchemaRef<Doc extends RequiredDoc, Schema> = Schema extends {
$ref: `#/components/schemas/${infer Name}`;
}
? 'schemas' extends keyof Doc['components']
? Name extends keyof Doc['components']['schemas']
? SchemaRef<Doc, Doc['components']['schemas'][Name]>
: never
: never
: {
[Key in keyof Schema]: SchemaRef<Doc, Schema[Key]>;
};
// @public (undocumented)
type ToTypeSafe<T> = UnknownIfNever<ConvertAll<TuplifyUnion<T>>[number]>;
// @public (undocumented)
type TuplifyUnion<
T,
L = LastOf<T>,
N = [T] extends [never] ? true : false,
> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>;
// @public
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never;
// @public (undocumented)
type UnknownIfNever<P> = [P] extends [never] ? unknown : P;
// @public
type ValueOf<T> = T[keyof T];
```
@@ -0,0 +1,40 @@
{
"name": "@backstage/backend-openapi-utils",
"description": "OpenAPI typescript support.",
"version": "0.0.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "node-library"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
],
"dependencies": {
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"json-schema-to-ts": "^2.6.2",
"openapi3-ts": "^3.1.2"
}
}
@@ -0,0 +1,25 @@
/*
* 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.
*/
/**
* Common functionalities for the openapi-router plugin.
*
* @packageDocumentation
*/
import * as internal from './types';
export { internal };
export type { ApiRouter } from './router';
@@ -0,0 +1,39 @@
/*
* 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 { Router } from 'express';
import { DocRequestMatcher, RequiredDoc } from './types';
/**
* Typed Express router based on an OpenAPI 3.1 spec.
* @public
*/
export interface ApiRouter<Doc extends RequiredDoc> extends Router {
get: DocRequestMatcher<Doc, this, 'get'>;
post: DocRequestMatcher<Doc, this, 'post'>;
all: DocRequestMatcher<Doc, this, 'all'>;
put: DocRequestMatcher<Doc, this, 'put'>;
delete: DocRequestMatcher<Doc, this, 'delete'>;
patch: DocRequestMatcher<Doc, this, 'patch'>;
options: DocRequestMatcher<Doc, this, 'options'>;
head: DocRequestMatcher<Doc, this, 'head'>;
}
@@ -0,0 +1,16 @@
/*
* 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 {};
@@ -0,0 +1,286 @@
/*
* 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.
*/
/**
* Pulled from https://github.com/varanauskas/oatx.
*/
import { FromSchema, JSONSchema7 } from 'json-schema-to-ts';
import {
ImmutableContentObject,
ImmutableOpenAPIObject,
ImmutableReferenceObject,
} from './immutable';
/**
* Basic OpenAPI spec with paths and components properties enforced.
* @public
*/
export type RequiredDoc = Pick<ImmutableOpenAPIObject, 'paths' | 'components'>;
/**
* @public
*/
export type PathDoc = Pick<ImmutableOpenAPIObject, 'paths'>;
/**
* Get value types of `T`.
* @public
*/
export type ValueOf<T> = T[keyof T];
/**
* Validate a string against OpenAPI path template, {@link https://spec.openapis.org/oas/v3.1.0#path-templating-matching}.
*
* @example
* ```ts
* const path: PathTemplate<"/posts/{postId}/comments/{commentId}"> = "/posts/:postId/comments/:commentId";
* const pathWithoutParams: PathTemplate<"/posts/comments"> = "/posts/comments";
* ```
*
* @public
*/
export type PathTemplate<Path extends string> =
Path extends `${infer Prefix}{${infer PathName}}${infer Suffix}`
? `${Prefix}:${PathName}${PathTemplate<Suffix>}`
: Path;
/**
* Extract path as specified in OpenAPI `Doc` based on request path
* @example
* ```ts
* const spec = {
* paths: {
* "/posts/{postId}/comments/{commentId}": {},
* "/posts/comments": {},
* }
* };
* const specPathWithParams: DocPath<typeof spec, "/posts/:postId/comments/:commentId"> = "/posts/{postId}/comments/{commentId}";
* const specPathWithoutParams: DocPath<typeof spec, "/posts/comments"> = "/posts/comments";
* ```
*
* @public
*/
export type DocPath<
Doc extends PathDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
> = ValueOf<{
[Template in Extract<
keyof Doc['paths'],
string
>]: Path extends PathTemplate<Template> ? Template : never;
}>;
/**
* @public
*/
export type DocPathTemplate<Doc extends PathDoc> = PathTemplate<
Extract<keyof Doc['paths'], string>
>;
/**
* @public
*/
export type DocPathMethod<
Doc extends Pick<RequiredDoc, 'paths'>,
Path extends DocPathTemplate<Doc>,
> = keyof Doc['paths'][DocPath<Doc, Path>];
/**
* @public
*/
export type MethodAwareDocPath<
Doc extends PathDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends keyof Doc['paths'][Path],
> = ValueOf<{
[Template in Extract<
keyof Doc['paths'],
string
>]: Path extends PathTemplate<Template>
? Method extends DocPathMethod<Doc, Path>
? PathTemplate<Template>
: never
: never;
}>;
/**
* @public
*/
export type DocOperation<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
> = Doc['paths'][Path][Method];
/**
* @public
*/
export type ComponentTypes<Doc extends RequiredDoc> = Extract<
keyof Doc['components'],
string
>;
/**
* @public
*/
export type ComponentRef<
Doc extends RequiredDoc,
Type extends ComponentTypes<Doc>,
Ref extends ImmutableReferenceObject,
> = Ref extends { $ref: `#/components/${Type}/${infer Name}` }
? Name extends keyof Doc['components'][Type]
? Doc['components'][Type][Name] extends ImmutableReferenceObject
? ComponentRef<Doc, Type, Doc['components'][Type][Name]>
: Doc['components'][Type][Name]
: never
: never;
/**
* @public
*/
export type SchemaRef<Doc extends RequiredDoc, Schema> = Schema extends {
$ref: `#/components/schemas/${infer Name}`;
}
? 'schemas' extends keyof Doc['components']
? Name extends keyof Doc['components']['schemas']
? SchemaRef<Doc, Doc['components']['schemas'][Name]>
: never
: never
: { [Key in keyof Schema]: SchemaRef<Doc, Schema[Key]> };
/**
* @public
*/
export type ObjectWithContentSchema<
Doc extends RequiredDoc,
Object extends { content?: ImmutableContentObject },
> = Object['content'] extends ImmutableContentObject
? SchemaRef<Doc, Object['content']['application/json']['schema']>
: never;
/**
* From {@link https://stackoverflow.com/questions/71393738/typescript-intersection-not-union-type-from-json-schema}
*
* StackOverflow says not to do this, but union types aren't possible any other way.
*
* @public
*/
export type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
/**
* @public
*/
export type LastOf<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer R
? R
: never;
/**
* @public
*/
export type Push<T extends any[], V> = [...T, V];
/**
* @public
*/
export type TuplifyUnion<
T,
L = LastOf<T>,
N = [T] extends [never] ? true : false,
> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>;
/**
* @public
*/
export type ConvertAll<T, R extends ReadonlyArray<unknown> = []> = T extends [
infer First extends JSONSchema7,
...infer Rest,
]
? ConvertAll<Rest, [...R, FromSchema<First>]>
: R;
/**
* @public
*/
export type UnknownIfNever<P> = [P] extends [never] ? unknown : P;
/**
* @public
*/
export type ToTypeSafe<T> = UnknownIfNever<ConvertAll<TuplifyUnion<T>>[number]>;
/**
* @public
*/
export type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = Extract<
T,
Record<K, V>
>;
/**
* @public
*/
export type MapDiscriminatedUnion<
T extends Record<K, string>,
K extends keyof T,
> = {
[V in T[K]]: DiscriminateUnion<T, K, V>;
};
/**
* @public
*/
export type PickOptionalKeys<T extends { [key: string]: any }> = {
[K in keyof T]: true extends T[K]['required'] ? never : K;
}[keyof T];
/**
* @public
*/
export type PickRequiredKeys<T extends { [key: string]: any }> = {
[K in keyof T]: true extends T[K]['required'] ? K : never;
}[keyof T];
/**
* @public
*/
export type OptionalMap<T extends { [key: string]: any }> = {
[P in Exclude<PickOptionalKeys<T>, undefined>]?: NonNullable<T[P]>;
};
/**
* @public
*/
export type RequiredMap<T extends { [key: string]: any }> = {
[P in Exclude<PickRequiredKeys<T>, undefined>]: NonNullable<T[P]>;
};
/**
* @public
*/
export type FullMap<T extends { [key: string]: any }> = RequiredMap<T> &
OptionalMap<T>;
/**
* @public
*/
export type Filter<T, U> = T extends U ? T : never;
@@ -0,0 +1,79 @@
/*
* 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 type core from 'express-serve-static-core';
import { DocPathTemplate, MethodAwareDocPath, RequiredDoc } from './common';
import { PathSchema, QuerySchema } from './params';
import { RequestBodyToJsonSchema } from './requests';
import { ResponseBodyToJsonSchema } from './responses';
/**
* Typed express request handler.
* @public
*/
export type DocRequestHandler<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends keyof Doc['paths'][Path],
> = core.RequestHandler<
PathSchema<Doc, Path, Method>,
ResponseBodyToJsonSchema<Doc, Path, Method>,
RequestBodyToJsonSchema<Doc, Path, Method>,
QuerySchema<Doc, Path, Method>,
Record<string, string>
>;
/**
* Typed express error handler / request handler union type.
* @public
*/
export type DocRequestHandlerParams<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends keyof Doc['paths'][Path],
> = core.RequestHandlerParams<
PathSchema<Doc, Path, Method>,
ResponseBodyToJsonSchema<Doc, Path, Method>,
RequestBodyToJsonSchema<Doc, Path, Method>,
QuerySchema<Doc, Path, Method>,
Record<string, string>
>;
/**
* Superset of the express router path matcher that enforces typed request and response bodies.
* @public
*/
export interface DocRequestMatcher<
Doc extends RequiredDoc,
T,
Method extends
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head',
> {
<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, Method>>(
path: Path,
...handlers: Array<DocRequestHandler<Doc, Path, Method>>
): T;
<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, Method>>(
path: Path,
...handlers: Array<DocRequestHandlerParams<Doc, Path, Method>>
): T;
}
@@ -0,0 +1,140 @@
/*
* 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 type {
ContentObject,
OpenAPIObject,
ReferenceObject,
RequestBodyObject,
ParameterObject,
SchemaObject,
ResponseObject,
} from 'openapi3-ts';
/**
* This file is meant to hold Immutable overwrites of the values provided by the `openapi3-ts`
* package due to issues with `as const` supporting only readonly values.
*/
/**
* From {@link https://github.com/microsoft/TypeScript/issues/13923#issuecomment-653675557}, allows
* us to convert from `as const` to the various OpenAPI types documented in `openapi3-ts`.
*
* @public
*/
export type Immutable<T> = T extends
| Function
| boolean
| number
| string
| null
| undefined
? T
: T extends Map<infer K, infer V>
? ReadonlyMap<Immutable<K>, Immutable<V>>
: T extends Set<infer S>
? ReadonlySet<Immutable<S>>
: { readonly [P in keyof T]: Immutable<T[P]> };
/**
* @public
*/
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
/**
* @public
*/
export type ImmutableReferenceObject = ImmutableObject<ReferenceObject>;
/**
* @public
*/
export type ImmutableOpenAPIObject = ImmutableObject<OpenAPIObject>;
/**
* @public
*/
export type ImmutableContentObject = ImmutableObject<ContentObject>;
/**
* @public
*/
export type ImmutableRequestBodyObject = ImmutableObject<RequestBodyObject>;
/**
* @public
*/
export type ImmutableResponseObject = ImmutableObject<ResponseObject>;
/**
* @public
*/
export type ImmutableParameterObject = ImmutableObject<ParameterObject>;
/**
* @public
*/
export interface HeaderObject extends ParameterObject {
in: 'header';
style: 'simple';
}
/**
* @public
*/
export type ImmutableHeaderObject = ImmutableObject<HeaderObject>;
/**
* @public
*/
export interface CookieObject extends ParameterObject {
in: 'cookie';
style?: 'form';
}
/**
* @public
*/
export type ImmutableCookieObject = ImmutableObject<CookieObject>;
/**
* @public
*/
export interface QueryObject extends ParameterObject {
in: 'query';
style?: 'form' | 'deepObject' | 'pipeDelimited' | 'spaceDelimited';
}
/**
* @public
*/
export type ImmutableQueryObject = ImmutableObject<QueryObject>;
/**
* @public
*/
export interface PathObject extends ParameterObject {
in: 'path';
style?: 'simple' | 'label' | 'matrix';
}
/**
* @public
*/
export type ImmutablePathObject = ImmutableObject<PathObject>;
/**
* @public
*/
export type ImmutableSchemaObject = ImmutableObject<SchemaObject>;
@@ -0,0 +1,22 @@
/*
* 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 * from './common';
export * from './express';
export * from './immutable';
export * from './params';
export * from './requests';
export * from './responses';
@@ -0,0 +1,171 @@
/*
* 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 {
ImmutableCookieObject,
ImmutableHeaderObject,
ImmutableParameterObject,
ImmutablePathObject,
ImmutableQueryObject,
ImmutableReferenceObject,
ImmutableSchemaObject,
} from './immutable';
import {
ComponentRef,
ComponentTypes,
DocOperation,
DocPath,
DocPathMethod,
Filter,
FullMap,
MapDiscriminatedUnion,
PathTemplate,
RequiredDoc,
} from './common';
import { FromSchema, JSONSchema7 } from 'json-schema-to-ts';
/**
* @public
*/
export type DocParameter<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
Parameter extends keyof Doc['paths'][Path][Method]['parameters'],
> = DocOperation<
Doc,
Path,
Method
>['parameters'][Parameter] extends ImmutableReferenceObject
? 'parameters' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'parameters',
DocOperation<Doc, Path, Method>['parameters'][Parameter]
>
: never
: DocOperation<Doc, Path, Method>['parameters'][Parameter];
/**
* @public
*/
export type DocParameters<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
> = DocOperation<Doc, Path, Method>['parameters'] extends ReadonlyArray<any>
? {
[Index in keyof DocOperation<
Doc,
Path,
Method
>['parameters']]: DocParameter<Doc, Path, Method, Index>;
}
: never;
/**
* @public
*/
export type ResolveDocParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
> = Schema extends ImmutableReferenceObject
? 'parameters' extends ComponentTypes<Doc>
? ComponentRef<Doc, 'parameters', Schema>
: never
: Schema;
/**
* @public
*/
export type ParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
> = ResolveDocParameterSchema<Doc, Schema> extends infer R
? R extends ImmutableSchemaObject
? R extends JSONSchema7
? FromSchema<R>
: never
: never
: never;
/**
* @public
*/
export type MapToSchema<
Doc extends RequiredDoc,
T extends Record<string, ImmutableParameterObject>,
> = {
[V in keyof T]: NonNullable<T[V]> extends ImmutableParameterObject
? ParameterSchema<Doc, NonNullable<T[V]>['schema']>
: never;
};
/**
* @public
*/
export type ParametersSchema<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
FilterType extends ImmutableParameterObject,
> = number extends keyof DocParameters<Doc, Path, Method>
? MapToSchema<
Doc,
FullMap<
MapDiscriminatedUnion<
Filter<DocParameters<Doc, Path, Method>[number], FilterType>,
'name'
>
>
>
: never;
/**
* @public
*/
export type HeaderSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableHeaderObject>;
/**
* @public
*/
export type CookieSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableCookieObject>;
/**
* @public
*/
export type PathSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutablePathObject>;
/**
* @public
*/
export type QuerySchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableQueryObject>;
@@ -0,0 +1,82 @@
/*
* 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.
*/
/**
* Pulled from https://github.com/varanauskas/oatx.
*/
import type {
ComponentRef,
ComponentTypes,
ObjectWithContentSchema,
RequiredDoc,
DocOperation,
DocPath,
DocPathMethod,
DocPathTemplate,
PathTemplate,
ToTypeSafe,
} from './common';
import {
ImmutableReferenceObject,
ImmutableRequestBodyObject,
} from './immutable';
/**
* @public
*/
export type RequestBody<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
> = DocOperation<
Doc,
Path,
Method
>['requestBody'] extends ImmutableReferenceObject
? 'requestBodies' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'requestBodies',
DocOperation<Doc, Path, Method>['requestBody']
>
: never
: DocOperation<Doc, Path, Method>['requestBody'];
/**
* @public
*/
export type RequestBodySchema<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = RequestBody<
Doc,
DocPath<Doc, Path>,
Method
> extends ImmutableRequestBodyObject
? ObjectWithContentSchema<Doc, RequestBody<Doc, DocPath<Doc, Path>, Method>>
: never;
/**
* Transform the OpenAPI request body schema to a typesafe JSON schema.
* @public
*/
export type RequestBodyToJsonSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ToTypeSafe<RequestBodySchema<Doc, Path, Method>>;
@@ -0,0 +1,83 @@
/*
* 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.
*/
/**
* Pulled from https://github.com/varanauskas/oatx.
*/
import type {
ComponentRef,
ComponentTypes,
ObjectWithContentSchema,
RequiredDoc,
DocOperation,
DocPathMethod,
DocPathTemplate,
PathTemplate,
ToTypeSafe,
ValueOf,
} from './common';
import { ImmutableReferenceObject, ImmutableResponseObject } from './immutable';
/**
* @public
*/
export type Response<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
StatusCode extends keyof DocOperation<Doc, Path, Method>['responses'],
> = DocOperation<
Doc,
Path,
Method
>['responses'][StatusCode] extends ImmutableReferenceObject
? 'responses' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'responses',
DocOperation<Doc, Path, Method>['responses'][StatusCode]
>
: never
: DocOperation<Doc, Path, Method>['responses'][StatusCode];
/**
* @public
*/
export type ResponseSchemas<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = {
[StatusCode in keyof DocOperation<Doc, Path, Method>['responses']]: Response<
Doc,
Path,
Method,
StatusCode
> extends ImmutableResponseObject
? ObjectWithContentSchema<Doc, Response<Doc, Path, Method, StatusCode>>
: never;
};
/**
* Transform the OpenAPI request body schema to a typesafe JSON schema.
* @public
*/
export type ResponseBodyToJsonSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ToTypeSafe<ValueOf<ResponseSchemas<Doc, Path, Method>>>;
+51
View File
@@ -1,5 +1,56 @@
# example-backend
## 0.2.82-next.3
### Patch Changes
- Updated dependencies
- @backstage/plugin-kubernetes-backend@0.10.0-next.3
- @backstage/plugin-catalog-backend@1.9.0-next.3
- @backstage/plugin-code-coverage-backend@0.2.10-next.3
- @backstage/plugin-lighthouse-backend@0.2.0-next.3
- @backstage/plugin-auth-backend@0.18.2-next.3
- @backstage/plugin-scaffolder-backend@1.13.0-next.3
- @backstage/plugin-search-backend-module-elasticsearch@1.2.0-next.3
- @backstage/catalog-model@1.3.0-next.0
- @backstage/plugin-events-backend@0.2.5-next.3
- example-app@0.2.82-next.3
- @backstage/backend-common@0.18.4-next.2
- @backstage/backend-tasks@0.5.1-next.2
- @backstage/catalog-client@1.4.1-next.1
- @backstage/config@1.0.7
- @backstage/integration@1.4.4-next.0
- @backstage/plugin-adr-backend@0.3.2-next.3
- @backstage/plugin-app-backend@0.3.44-next.2
- @backstage/plugin-auth-node@0.2.13-next.2
- @backstage/plugin-azure-devops-backend@0.3.23-next.2
- @backstage/plugin-azure-sites-backend@0.1.6-next.2
- @backstage/plugin-badges-backend@0.1.38-next.3
- @backstage/plugin-catalog-node@1.3.5-next.3
- @backstage/plugin-entity-feedback-backend@0.1.2-next.3
- @backstage/plugin-events-node@0.2.5-next.2
- @backstage/plugin-explore-backend@0.0.6-next.2
- @backstage/plugin-graphql-backend@0.1.34-next.3
- @backstage/plugin-jenkins-backend@0.1.34-next.3
- @backstage/plugin-kafka-backend@0.2.37-next.3
- @backstage/plugin-linguist-backend@0.2.1-next.3
- @backstage/plugin-permission-backend@0.5.19-next.2
- @backstage/plugin-permission-common@0.7.5-next.0
- @backstage/plugin-permission-node@0.7.7-next.2
- @backstage/plugin-playlist-backend@0.2.7-next.3
- @backstage/plugin-proxy-backend@0.2.38-next.2
- @backstage/plugin-rollbar-backend@0.1.41-next.2
- @backstage/plugin-scaffolder-backend-module-rails@0.4.12-next.3
- @backstage/plugin-search-backend@1.3.0-next.2
- @backstage/plugin-search-backend-module-pg@0.5.5-next.2
- @backstage/plugin-search-backend-node@1.2.0-next.2
- @backstage/plugin-search-common@1.2.3-next.0
- @backstage/plugin-tech-insights-backend@0.5.10-next.3
- @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.28-next.2
- @backstage/plugin-tech-insights-node@0.4.2-next.2
- @backstage/plugin-techdocs-backend@1.6.1-next.3
- @backstage/plugin-todo-backend@0.1.41-next.3
## 0.2.82-next.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.2.82-next.2",
"version": "0.2.82-next.3",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
+8 -2
View File
@@ -19,11 +19,17 @@ import { PluginEnvironment } from '../types';
import { CatalogClient } from '@backstage/catalog-client';
export default async function createPlugin(env: PluginEnvironment) {
const { logger, scheduler, config } = env;
const { logger, scheduler, config, tokenManager } = env;
const catalogClient = new CatalogClient({
discoveryApi: env.discovery,
});
await createScheduler({ logger, scheduler, config, catalogClient });
await createScheduler({
logger,
scheduler,
config,
catalogClient,
tokenManager,
});
}
+8
View File
@@ -1,5 +1,13 @@
# @backstage/catalog-client
## 1.4.1-next.1
### Patch Changes
- Updated dependencies
- @backstage/catalog-model@1.3.0-next.0
- @backstage/errors@1.1.5
## 1.4.1-next.0
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/catalog-client",
"description": "An isomorphic client for the catalog backend",
"version": "1.4.1-next.0",
"version": "1.4.1-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+13
View File
@@ -1,5 +1,18 @@
# @backstage/catalog-model
## 1.3.0-next.0
### Minor Changes
- be9c4228073: Modified the regex for DNS label validation to support IDN domains
### Patch Changes
- Updated dependencies
- @backstage/config@1.0.7
- @backstage/errors@1.1.5
- @backstage/types@1.0.2
## 1.2.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/catalog-model",
"description": "Types and validators that help describe the model of a Backstage Catalog",
"version": "1.2.1",
"version": "1.3.0-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+14
View File
@@ -0,0 +1,14 @@
# @backstage/cli-node
## 0.1.0-next.0
### Minor Changes
- 3156b0d85dc: Introduced the new `@backstage/cli-node` package, which provides utilities for use across Backstage CLIs.
### Patch Changes
- Updated dependencies
- @backstage/cli-common@0.1.12
- @backstage/errors@1.1.5
- @backstage/types@1.0.2
+10
View File
@@ -0,0 +1,10 @@
# @backstage/cli-node
This library provides utilities for building CLI tools for Backstage.
The difference between this library and `@backstage/cli-common` is that this library is more feature rich with a larger dependency tree, with less concern for bundle size and installation speed. The `@backstage/cli-common` package on the other hand is intended to be extremely slim and only provide minimal features for use in tools like `@backstage/create-app`.
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
- [Backstage Documentation](https://backstage.io/docs)
+167
View File
@@ -0,0 +1,167 @@
## API Report File for "@backstage/cli-node"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { JsonValue } from '@backstage/types';
import { Package } from '@manypkg/get-packages';
// @public
export type BackstagePackage = {
dir: string;
packageJson: BackstagePackageJson;
};
// @public
export interface BackstagePackageJson {
// (undocumented)
backstage?: {
role?: PackageRole;
};
// (undocumented)
bundled?: boolean;
// (undocumented)
dependencies?: {
[key: string]: string;
};
// (undocumented)
devDependencies?: {
[key: string]: string;
};
// (undocumented)
exports?: JsonValue;
// (undocumented)
files?: string[];
// (undocumented)
main?: string;
// (undocumented)
module?: string;
// (undocumented)
name: string;
// (undocumented)
optionalDependencies?: {
[key: string]: string;
};
// (undocumented)
peerDependencies?: {
[key: string]: string;
};
// (undocumented)
private?: boolean;
// (undocumented)
publishConfig?: {
access?: 'public' | 'restricted';
directory?: string;
registry?: string;
alphaTypes?: string;
betaTypes?: string;
};
// (undocumented)
scripts?: {
[key: string]: string;
};
// (undocumented)
types?: string;
// (undocumented)
typesVersions?: Record<string, Record<string, string[]>>;
// (undocumented)
version: string;
}
// @public
export class GitUtils {
static listChangedFiles(ref: string): Promise<string[]>;
static readFileAtRef(path: string, ref: string): Promise<string>;
}
// @public
export function isMonoRepo(): Promise<boolean>;
// @public
export class Lockfile {
createSimplifiedDependencyGraph(): Map<string, Set<string>>;
diff(otherLockfile: Lockfile): LockfileDiff;
static load(path: string): Promise<Lockfile>;
static parse(content: string): Lockfile;
}
// @public
export type LockfileDiff = {
added: LockfileDiffEntry[];
changed: LockfileDiffEntry[];
removed: LockfileDiffEntry[];
};
// @public
export type LockfileDiffEntry = {
name: string;
range: string;
};
// @public
export class PackageGraph extends Map<string, PackageGraphNode> {
collectPackageNames(
startingPackageNames: string[],
collectFn: (pkg: PackageGraphNode) => Iterable<string> | undefined,
): Set<string>;
static fromPackages(packages: Package[]): PackageGraph;
listChangedPackages(options: {
ref: string;
analyzeLockfile?: boolean;
}): Promise<PackageGraphNode[]>;
static listTargetPackages(): Promise<BackstagePackage[]>;
}
// @public
export type PackageGraphNode = {
name: string;
dir: string;
packageJson: BackstagePackageJson;
allLocalDependencies: Map<string, PackageGraphNode>;
publishedLocalDependencies: Map<string, PackageGraphNode>;
localDependencies: Map<string, PackageGraphNode>;
localDevDependencies: Map<string, PackageGraphNode>;
localOptionalDependencies: Map<string, PackageGraphNode>;
allLocalDependents: Map<string, PackageGraphNode>;
publishedLocalDependents: Map<string, PackageGraphNode>;
localDependents: Map<string, PackageGraphNode>;
localDevDependents: Map<string, PackageGraphNode>;
localOptionalDependents: Map<string, PackageGraphNode>;
};
// @public
export type PackageOutputType = 'bundle' | 'types' | 'esm' | 'cjs';
// @public
export type PackagePlatform = 'node' | 'web' | 'common';
// @public
export type PackageRole =
| 'frontend'
| 'backend'
| 'cli'
| 'web-library'
| 'node-library'
| 'common-library'
| 'frontend-plugin'
| 'frontend-plugin-module'
| 'backend-plugin'
| 'backend-plugin-module';
// @public
export interface PackageRoleInfo {
// (undocumented)
output: PackageOutputType[];
// (undocumented)
platform: PackagePlatform;
// (undocumented)
role: PackageRole;
}
// @public
export class PackageRoles {
static detectRoleFromPackage(pkgJson: unknown): PackageRole | undefined;
static getRoleFromPackage(pkgJson: unknown): PackageRole | undefined;
static getRoleInfo(role: string): PackageRoleInfo;
}
```
+41
View File
@@ -0,0 +1,41 @@
{
"name": "@backstage/cli-node",
"description": "Node.js library for Backstage CLIs",
"version": "0.1.0-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "node-library"
},
"scripts": {
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/cli-common": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/types": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
"@yarnpkg/parsers": "^3.0.0-rc.4",
"fs-extra": "10.1.0",
"semver": "^7.3.2",
"zod": "^3.21.4"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"mock-fs": "^5.2.0"
},
"files": [
"dist"
]
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { runGit, listChangedFiles } from './git';
import { runGit, GitUtils } from './GitUtils';
describe('runGit', () => {
it('runs a git command', async () => {
@@ -43,10 +43,14 @@ describe('runGit', () => {
describe('listChangedFiles', () => {
it('requires a ref', async () => {
await expect(listChangedFiles('')).rejects.toThrow('ref is required');
await expect(GitUtils.listChangedFiles('')).rejects.toThrow(
'ref is required',
);
});
it('should return something', async () => {
await expect(listChangedFiles('HEAD')).resolves.toEqual(expect.any(Array));
await expect(GitUtils.listChangedFiles('HEAD')).resolves.toEqual(
expect.any(Array),
);
});
});
+94
View File
@@ -0,0 +1,94 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assertError, ForwardedError } from '@backstage/errors';
import { paths } from '../paths';
import { execFile } from '../run';
/**
* Run a git command, trimming the output splitting it into lines.
*/
export async function runGit(...args: string[]) {
try {
const { stdout } = await execFile('git', args, {
shell: true,
cwd: paths.targetRoot,
});
return stdout.trim().split(/\r\n|\r|\n/);
} catch (error) {
assertError(error);
if (error.stderr || typeof error.code === 'number') {
const stderr = (error.stderr as undefined | Buffer)?.toString('utf8');
const msg = stderr?.trim() ?? `with exit code ${error.code}`;
throw new Error(`git ${args[0]} failed, ${msg}`);
}
throw new ForwardedError('Unknown execution error', error);
}
}
/**
* Utilities for working with git.
*
* @public
*/
export class GitUtils {
/**
* Returns a sorted list of all files that have changed since the merge base
* of the provided `ref` and HEAD, as well as all files that are not tracked by git.
*/
static async listChangedFiles(ref: string) {
if (!ref) {
throw new Error('ref is required');
}
let diffRef = ref;
try {
const [base] = await runGit('merge-base', 'HEAD', ref);
diffRef = base;
} catch {
// silently fall back to using the ref directly if merge base is not available
}
const tracked = await runGit('diff', '--name-only', diffRef);
const untracked = await runGit(
'ls-files',
'--others',
'--exclude-standard',
);
return Array.from(new Set([...tracked, ...untracked]));
}
/**
* Returns the contents of a file at a specific ref.
*/
static async readFileAtRef(path: string, ref: string) {
let showRef = ref;
try {
const [base] = await runGit('merge-base', 'HEAD', ref);
showRef = base;
} catch {
// silently fall back to using the ref directly if merge base is not available
}
const { stdout } = await execFile('git', ['show', `${showRef}:${path}`], {
shell: true,
cwd: paths.targetRoot,
maxBuffer: 1024 * 1024 * 50,
});
return stdout;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2022 The Backstage Authors
* 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.
@@ -14,9 +14,4 @@
* limitations under the License.
*/
export { PackageGraph } from './PackageGraph';
export type {
PackageGraphNode,
ExtendedPackage,
ExtendedPackageJSON,
} from './PackageGraph';
export { GitUtils } from './GitUtils';
+25
View File
@@ -0,0 +1,25 @@
/*
* 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.
*/
/**
* Node.js library for Backstage CLIs
*
* @packageDocumentation
*/
export * from './git';
export * from './monorepo';
export * from './roles';
@@ -0,0 +1,501 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import mockFs from 'mock-fs';
import { Lockfile } from './Lockfile';
const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
`;
const MODERN_HEADER = `# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 6
cacheKey: 8
`;
describe('New Lockfile', () => {
afterEach(() => {
mockFs.restore();
});
describe('diff', () => {
const lockfileLegacyA = Lockfile.parse(`${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
dependencies:
b "^2"
b@3:
version "3.0.1"
integrity sha512-abc1
b@2.0.x:
version "2.0.1"
integrity sha512-abc2
b@^2:
version "2.0.0"
integrity sha512-abc3
c@^1:
version "1.0.1"
integrity x
`);
const lockfileLegacyB = Lockfile.parse(`${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz-other
dependencies:
b "^2"
b@2.0.x, b@^2:
version "2.0.0"
integrity sha512-abc3
b@4:
version "4.0.0"
integrity sha512-abc
d@^1:
version "1.0.1"
integrity x
`);
const lockfileModernA = Lockfile.parse(`${MODERN_HEADER}
"a@npm:^1":
version: "1.0.1"
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
checksum: sha512-xyz
dependencies:
b: "^2"
"b@npm:3":
version: "3.0.1"
checksum: sha512-abc1
"b@npm:2.0.x":
version: "2.0.1"
checksum: sha512-abc2
"b@npm:^2":
version: "2.0.0"
checksum: sha512-abc3
"c@npm:^1":
version: "1.0.1"
checksum: x
`);
const lockfileModernB = Lockfile.parse(`${MODERN_HEADER}
"a@npm:^1":
version: "1.0.1"
resolution: "a@npm:1.0.1"
checksum: sha512-xyz-other
dependencies:
b: "^2"
"b@npm:2.0.x, b@npm:^2":
version: "2.0.0"
checksum: sha512-abc3
"b@npm:4":
version: "4.0.0"
checksum: sha512-abc
"d@npm:^1":
version: "1.0.1"
checksum: x
`);
it('should diff two legacy lockfiles', async () => {
expect(lockfileLegacyA.diff(lockfileLegacyB)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
expect(lockfileLegacyB.diff(lockfileLegacyA)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
});
it('should diff two modern lockfiles', async () => {
expect(lockfileModernA.diff(lockfileModernB)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
expect(lockfileModernB.diff(lockfileModernA)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
});
it('should diff legacy and modern lockfiles', async () => {
expect(lockfileLegacyA.diff(lockfileModernB)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
expect(lockfileLegacyB.diff(lockfileModernA)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
});
it('should diff modern and legacy lockfiles', async () => {
expect(lockfileModernA.diff(lockfileLegacyB)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
expect(lockfileModernB.diff(lockfileLegacyA)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
});
it('should handle workspace ranges', async () => {
const lockfile = `${MODERN_HEADER}
"@backstage/app-defaults@workspace:^, @backstage/app-defaults@workspace:packages/app-defaults":
version: 0.0.0-use.local
resolution: "@backstage/app-defaults@workspace:packages/app-defaults"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.12.2
"@material-ui/icons": ^4.9.1
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@types/node": ^16.11.26
"@types/react": ^16.13.1 || ^17.0.0
peerDependencies:
react: ^16.13.1 || ^17.0.0
react-dom: ^16.13.1 || ^17.0.0
react-router-dom: 6.0.0-beta.0 || ^6.3.0
languageName: unknown
linkType: soft
"@backstage/backend-app-api@workspace:^, @backstage/backend-app-api@workspace:packages/backend-app-api":
version: 0.0.0-use.local
resolution: "@backstage/backend-app-api@workspace:packages/backend-app-api"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
express: ^4.17.1
express-promise-router: ^4.1.0
winston: ^3.2.1
languageName: unknown
linkType: soft
`;
expect(Lockfile.parse(lockfile).diff(Lockfile.parse(lockfile))).toEqual({
added: [],
changed: [],
removed: [],
});
});
});
describe('createSimplifiedDependencyGraph', () => {
it('for modern lockfile', () => {
expect(
Lockfile.parse(
`${MODERN_HEADER}
"@backstage/app-defaults@workspace:^, @backstage/app-defaults@workspace:packages/app-defaults":
version: 0.0.0-use.local
resolution: "@backstage/app-defaults@workspace:packages/app-defaults"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.12.2
"@material-ui/icons": ^4.9.1
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@types/node": ^16.11.26
"@types/react": ^16.13.1 || ^17.0.0
peerDependencies:
react: ^16.13.1 || ^17.0.0
react-dom: ^16.13.1 || ^17.0.0
react-router-dom: 6.0.0-beta.0 || ^6.3.0
languageName: unknown
linkType: soft
"@backstage/backend-app-api@workspace:^, @backstage/backend-app-api@workspace:packages/backend-app-api":
version: 0.0.0-use.local
resolution: "@backstage/backend-app-api@workspace:packages/backend-app-api"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
express: ^4.17.1
express-promise-router: ^4.1.0
winston: ^3.2.1
languageName: unknown
linkType: soft
`,
).createSimplifiedDependencyGraph(),
).toEqual(
new Map([
[
'@backstage/app-defaults',
new Set([
'@backstage/cli',
'@backstage/core-app-api',
'@backstage/core-components',
'@backstage/core-plugin-api',
'@backstage/plugin-permission-react',
'@backstage/test-utils',
'@backstage/theme',
'@material-ui/core',
'@material-ui/icons',
'@testing-library/jest-dom',
'@testing-library/react',
'@types/node',
'@types/react',
'react',
'react-dom',
'react-router-dom',
]),
],
[
'@backstage/backend-app-api',
new Set([
'@backstage/backend-common',
'@backstage/backend-plugin-api',
'@backstage/backend-tasks',
'@backstage/cli',
'@backstage/errors',
'@backstage/plugin-permission-node',
'express',
'express-promise-router',
'winston',
]),
],
]),
);
});
it('for simple lockfile without dependencies', () => {
expect(
Lockfile.parse(
`${MODERN_HEADER}
"a@npm:^1":
version: "1.0.1"
"b@npm:3":
version: "3.0.1"
"b@npm:2.0.x":
version: "2.0.1"
checksum: sha512-abc2
`,
).createSimplifiedDependencyGraph(),
).toEqual(
new Map([
['a', new Set()],
['b', new Set()],
]),
);
});
it('for lockfile with dependencies', () => {
expect(
Lockfile.parse(
`${MODERN_HEADER}
"a@npm:^1":
version: "1.0.1"
dependencies:
b: "^2"
"b@npm:3":
version: "3.0.1"
checksum: sha512-abc1
"b@npm:2.0.x":
version: "2.0.1"
checksum: sha512-abc2
dependencies:
c: "^1"
"b@npm:^2":
version: "2.0.0"
checksum: sha512-abc3
peerDependencies:
d: "^1"
"c@npm:^1":
version: "1.0.1"
"d@npm:^1":
version: "1.0.2"
`,
).createSimplifiedDependencyGraph(),
).toEqual(
new Map([
['a', new Set(['b'])],
['b', new Set(['c', 'd'])],
['c', new Set()],
['d', new Set()],
]),
);
});
it('for legacy lockfile', () => {
expect(
Lockfile.parse(
`${LEGACY_HEADER}
a@^1:
version "1.0.1"
dependencies:
b "^2"
b@3:
version "3.0.1"
integrity sha512-abc1
b@2.0.x:
version "2.0.1"
integrity sha512-abc2
dependencies:
c "^1"
b@^2:
version "2.0.0"
integrity sha512-abc3
dependencies:
d "^1"
c@^1:
version "1.0.1"
integrity x
d@^1:
version "1.0.1"
integrity x
`,
).createSimplifiedDependencyGraph(),
).toEqual(
new Map([
['a', new Set(['b'])],
['b', new Set(['c', 'd'])],
['c', new Set()],
['d', new Set()],
]),
);
});
});
});
+218
View File
@@ -0,0 +1,218 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { parseSyml } from '@yarnpkg/parsers';
import fs from 'fs-extra';
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
/** @internal */
type LockfileData = {
[entry: string]: {
version: string;
resolved?: string;
integrity?: string /* old */;
checksum?: string /* new */;
dependencies?: { [name: string]: string };
peerDependencies?: { [name: string]: string };
};
};
/** @internal */
type LockfileQueryEntry = {
range: string;
version: string;
dataKey: string;
};
/**
* An entry for a single difference between two {@link Lockfile}s.
*
* @public
*/
export type LockfileDiffEntry = {
name: string;
range: string;
};
/**
* Represents the difference between two {@link Lockfile}s.
*
* @public
*/
export type LockfileDiff = {
added: LockfileDiffEntry[];
changed: LockfileDiffEntry[];
removed: LockfileDiffEntry[];
};
// these are special top level yarn keys.
// https://github.com/yarnpkg/berry/blob/9bd61fbffb83d0b8166a9cc26bec3a58743aa453/packages/yarnpkg-parsers/sources/syml.ts#L9
const SPECIAL_OBJECT_KEYS = [
`__metadata`,
`version`,
`resolution`,
`dependencies`,
`peerDependencies`,
`dependenciesMeta`,
`peerDependenciesMeta`,
`binaries`,
];
/**
* Represents a package manager lockfile.
*
* @public
*/
export class Lockfile {
/**
* Load a {@link Lockfile} from a file path.
*/
static async load(path: string): Promise<Lockfile> {
const lockfileContents = await fs.readFile(path, 'utf8');
return Lockfile.parse(lockfileContents);
}
/**
* Parse lockfile contents into a {@link Lockfile}.
*
* @public
*/
static parse(content: string): Lockfile {
let data: LockfileData;
try {
data = parseSyml(content);
} catch (err) {
throw new Error(`Failed yarn.lock parse, ${err}`);
}
const packages = new Map<string, LockfileQueryEntry[]>();
for (const [key, value] of Object.entries(data)) {
if (SPECIAL_OBJECT_KEYS.includes(key)) continue;
const [, name, ranges] = ENTRY_PATTERN.exec(key) ?? [];
if (!name) {
throw new Error(`Failed to parse yarn.lock entry '${key}'`);
}
let queries = packages.get(name);
if (!queries) {
queries = [];
packages.set(name, queries);
}
for (let range of ranges.split(/\s*,\s*/)) {
if (range.startsWith(`${name}@`)) {
range = range.slice(`${name}@`.length);
}
if (range.startsWith('npm:')) {
range = range.slice('npm:'.length);
}
queries.push({ range, version: value.version, dataKey: key });
}
}
return new Lockfile(packages, data);
}
private constructor(
private readonly packages: Map<string, LockfileQueryEntry[]>,
private readonly data: LockfileData,
) {}
/**
* Creates a simplified dependency graph from the lockfile data, where each
* key is a package, and the value is a set of all packages that it depends on
* across all versions.
*/
createSimplifiedDependencyGraph(): Map<string, Set<string>> {
const graph = new Map<string, Set<string>>();
for (const [name, entries] of this.packages) {
const dependencies = new Set(
entries.flatMap(e => {
const data = this.data[e.dataKey];
return [
...Object.keys(data?.dependencies ?? {}),
...Object.keys(data?.peerDependencies ?? {}),
];
}),
);
graph.set(name, dependencies);
}
return graph;
}
/**
* Diff with another lockfile, returning entries that have been
* added, changed, and removed compared to the other lockfile.
*/
diff(otherLockfile: Lockfile): LockfileDiff {
const diff = {
added: new Array<{ name: string; range: string }>(),
changed: new Array<{ name: string; range: string }>(),
removed: new Array<{ name: string; range: string }>(),
};
// Keeps track of packages that only exist in this lockfile
const remainingOldNames = new Set(this.packages.keys());
for (const [name, otherQueries] of otherLockfile.packages) {
remainingOldNames.delete(name);
const thisQueries = this.packages.get(name);
// If the packages doesn't exist in this lockfile, add all entries
if (!thisQueries) {
diff.removed.push(...otherQueries.map(q => ({ name, range: q.range })));
continue;
}
const remainingOldRanges = new Set(thisQueries.map(q => q.range));
for (const otherQuery of otherQueries) {
remainingOldRanges.delete(otherQuery.range);
const thisQuery = thisQueries.find(q => q.range === otherQuery.range);
if (!thisQuery) {
diff.removed.push({ name, range: otherQuery.range });
continue;
}
const otherPkg = otherLockfile.data[otherQuery.dataKey];
const thisPkg = this.data[thisQuery.dataKey];
if (otherPkg && thisPkg) {
const thisCheck = thisPkg.integrity || thisPkg.checksum;
const otherCheck = otherPkg.integrity || otherPkg.checksum;
if (thisCheck !== otherCheck) {
diff.changed.push({ name, range: otherQuery.range });
}
}
}
for (const thisRange of remainingOldRanges) {
diff.added.push({ name, range: thisRange });
}
}
for (const name of remainingOldNames) {
const queries = this.packages.get(name) ?? [];
diff.added.push(...queries.map(q => ({ name, range: q.range })));
}
return diff;
}
}
@@ -17,17 +17,11 @@
import { resolve as resolvePath } from 'path';
import { getPackages } from '@manypkg/get-packages';
import { PackageGraph } from './PackageGraph';
import { Lockfile } from '../versioning/Lockfile';
import { listChangedFiles, readFileAtRef } from '../git';
import { Lockfile } from './Lockfile';
import { GitUtils } from '../git';
jest.mock('../git');
const mockListChangedFiles = listChangedFiles as jest.MockedFunction<
typeof listChangedFiles
>;
const mockReadFileAtRef = readFileAtRef as jest.MockedFunction<
typeof readFileAtRef
>;
const mockListChangedFiles = jest.spyOn(GitUtils, 'listChangedFiles');
const mockReadFileAtRef = jest.spyOn(GitUtils, 'readFileAtRef');
jest.mock('../paths', () => ({
paths: {
@@ -17,14 +17,21 @@
import path from 'path';
import { getPackages, Package } from '@manypkg/get-packages';
import { paths } from '../paths';
import { PackageRole } from '../role';
import { listChangedFiles, readFileAtRef } from '../git';
import { Lockfile } from '../versioning';
import { PackageRole } from '../roles';
import { GitUtils } from '../git';
import { Lockfile } from './Lockfile';
import { JsonValue } from '@backstage/types';
type PackageJSON = Package['packageJson'];
/**
* Known fields in Backstage package.json files.
*
* @public
*/
export interface BackstagePackageJson {
name: string;
version: string;
private?: boolean;
export interface ExtendedPackageJSON extends PackageJSON {
main?: string;
module?: string;
types?: string;
@@ -52,20 +59,43 @@ export interface ExtendedPackageJSON extends PackageJSON {
alphaTypes?: string;
betaTypes?: string;
};
dependencies?: {
[key: string]: string;
};
peerDependencies?: {
[key: string]: string;
};
devDependencies?: {
[key: string]: string;
};
optionalDependencies?: {
[key: string]: string;
};
}
export type ExtendedPackage = {
/**
* A local Backstage monorepo package
*
* @public
*/
export type BackstagePackage = {
dir: string;
packageJson: ExtendedPackageJSON;
packageJson: BackstagePackageJson;
};
/**
* A local package in the monorepo package graph.
*
* @public
*/
export type PackageGraphNode = {
/** The name of the package */
name: string;
/** The directory of the package */
dir: string;
/** The package data of the package itself */
packageJson: ExtendedPackageJSON;
packageJson: BackstagePackageJson;
/** All direct local dependencies of the package */
allLocalDependencies: Map<string, PackageGraphNode>;
@@ -90,12 +120,23 @@ export type PackageGraphNode = {
localOptionalDependents: Map<string, PackageGraphNode>;
};
/**
* Represents a local Backstage monorepo package graph.
*
* @public
*/
export class PackageGraph extends Map<string, PackageGraphNode> {
static async listTargetPackages(): Promise<ExtendedPackage[]> {
/**
* Lists all local packages in a monorepo.
*/
static async listTargetPackages(): Promise<BackstagePackage[]> {
const { packages } = await getPackages(paths.targetDir);
return packages as ExtendedPackage[];
return packages as BackstagePackage[];
}
/**
* Creates a package graph from a list of local packages.
*/
static fromPackages(packages: Package[]): PackageGraph {
const graph = new PackageGraph();
@@ -112,7 +153,7 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
graph.set(name, {
name,
dir: pkg.dir,
packageJson: pkg.packageJson as ExtendedPackageJSON,
packageJson: pkg.packageJson as BackstagePackageJson,
allLocalDependencies: new Map(),
publishedLocalDependencies: new Map(),
@@ -210,11 +251,19 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
return targets;
}
/**
* Lists all packages that have changed since a given git ref.
*
* @remarks
*
* If the `analyzeLockfile` option is set to true, the change detection will
* also consider changes to the dependency management lockfile.
*/
async listChangedPackages(options: {
ref: string;
analyzeLockfile?: boolean;
}) {
const changedFiles = await listChangedFiles(options.ref);
const changedFiles = await GitUtils.listChangedFiles(options.ref);
const dirMap = new Map(
Array.from(this.values()).map(pkg => [
@@ -265,7 +314,7 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
paths.resolveTargetRoot('yarn.lock'),
);
otherLockfile = Lockfile.parse(
await readFileAtRef('yarn.lock', options.ref),
await GitUtils.readFileAtRef('yarn.lock', options.ref),
);
} catch (error) {
console.warn(
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { isMonoRepo } from './isMonoRepo';
export {
PackageGraph,
type PackageGraphNode,
type BackstagePackage,
type BackstagePackageJson,
} from './PackageGraph';
export {
Lockfile,
type LockfileDiff,
type LockfileDiffEntry,
} from './Lockfile';
@@ -17,6 +17,11 @@
import { paths } from '../paths';
import fs from 'fs-extra';
/**
* Returns try if the current project is a monorepo.
*
* @public
*/
export async function isMonoRepo(): Promise<boolean> {
const rootPackageJsonPath = paths.resolveTargetRoot('package.json');
try {
@@ -14,9 +14,7 @@
* limitations under the License.
*/
export { Tabs } from './Tabs';
export type { TabsClassKey } from './Tabs';
export type { TabClassKey } from './Tab';
import { findPaths } from '@backstage/cli-common';
export type { TabBarClassKey } from './TabBar';
export type { TabIconClassKey } from './TabIcon';
/* eslint-disable-next-line no-restricted-syntax */
export const paths = findPaths(__dirname);
@@ -14,30 +14,23 @@
* limitations under the License.
*/
import mockFs from 'mock-fs';
import { Command } from 'commander';
import {
getRoleInfo,
getRoleFromPackage,
findRoleFromCommand,
detectRoleFromPackage,
} from './packageRoles';
import { PackageRoles } from './PackageRoles';
describe('getRoleInfo', () => {
it('provides role info by role', () => {
expect(getRoleInfo('web-library')).toEqual({
expect(PackageRoles.getRoleInfo('web-library')).toEqual({
role: 'web-library',
platform: 'web',
output: ['types', 'esm'],
});
expect(getRoleInfo('frontend')).toEqual({
expect(PackageRoles.getRoleInfo('frontend')).toEqual({
role: 'frontend',
platform: 'web',
output: ['bundle'],
});
expect(() => getRoleInfo('invalid')).toThrow(
expect(() => PackageRoles.getRoleInfo('invalid')).toThrow(
`Unknown package role 'invalid'`,
);
});
@@ -46,7 +39,7 @@ describe('getRoleInfo', () => {
describe('getRoleFromPackage', () => {
it('reads explicit package roles', () => {
expect(
getRoleFromPackage({
PackageRoles.getRoleFromPackage({
backstage: {
role: 'web-library',
},
@@ -54,7 +47,7 @@ describe('getRoleFromPackage', () => {
).toEqual('web-library');
expect(
getRoleFromPackage({
PackageRoles.getRoleFromPackage({
backstage: {
role: 'frontend',
},
@@ -62,14 +55,14 @@ describe('getRoleFromPackage', () => {
).toEqual('frontend');
expect(() =>
getRoleFromPackage({
PackageRoles.getRoleFromPackage({
name: 'test',
backstage: {},
}),
).toThrow('Package test must specify a role in the "backstage" field');
expect(() =>
getRoleFromPackage({
PackageRoles.getRoleFromPackage({
name: 'test',
backstage: { role: 'invalid' },
}),
@@ -77,48 +70,10 @@ describe('getRoleFromPackage', () => {
});
});
describe('findRoleFromCommand', () => {
function mkCommand(args: string) {
const parsed = new Command()
.option('--role <role>', 'test role')
.parse(['node', 'entry.js', ...args.split(' ')]) as Command;
return parsed.opts();
}
beforeEach(() => {
mockFs({
'package.json': JSON.stringify({
name: 'test',
backstage: {
role: 'web-library',
},
}),
});
});
afterEach(() => {
mockFs.restore();
});
it('provides role info by role', async () => {
await expect(findRoleFromCommand(mkCommand(''))).resolves.toEqual(
'web-library',
);
await expect(
findRoleFromCommand(mkCommand('--role node-library')),
).resolves.toEqual('node-library');
await expect(
findRoleFromCommand(mkCommand('--role invalid')),
).rejects.toThrow(`Unknown package role 'invalid'`);
});
});
describe('detectRoleFromPackage', () => {
it('detects the role of example-app', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: 'example-app',
private: true,
bundled: true,
@@ -141,7 +96,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of example-backend', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: 'example-backend',
main: 'dist/index.cjs.js',
types: 'src/index.ts',
@@ -160,7 +115,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-catalog', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-catalog',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -185,7 +140,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-catalog-backend', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-catalog-backend',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -209,7 +164,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-catalog-react', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-catalog-react',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -232,7 +187,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-catalog-common', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-catalog-common',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -256,7 +211,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-catalog-backend-module-ldap', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-catalog-backend-module-ldap',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -279,7 +234,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-permission-node', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-permission-node',
main: 'src/index.ts',
types: 'src/index.ts',
@@ -303,7 +258,7 @@ describe('detectRoleFromPackage', () => {
it('detects the role of @backstage/plugin-analytics-module-ga', () => {
expect(
detectRoleFromPackage({
PackageRoles.detectRoleFromPackage({
name: '@backstage/plugin-analytics-module-ga',
main: 'src/index.ts',
types: 'src/index.ts',
+186
View File
@@ -0,0 +1,186 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import { PackageRole, PackageRoleInfo } from './types';
const packageRoleInfos: PackageRoleInfo[] = [
{
role: 'frontend',
platform: 'web',
output: ['bundle'],
},
{
role: 'backend',
platform: 'node',
output: ['bundle'],
},
{
role: 'cli',
platform: 'node',
output: ['cjs'],
},
{
role: 'web-library',
platform: 'web',
output: ['types', 'esm'],
},
{
role: 'node-library',
platform: 'node',
output: ['types', 'cjs'],
},
{
role: 'common-library',
platform: 'common',
output: ['types', 'esm', 'cjs'],
},
{
role: 'frontend-plugin',
platform: 'web',
output: ['types', 'esm'],
},
{
role: 'frontend-plugin-module',
platform: 'web',
output: ['types', 'esm'],
},
{
role: 'backend-plugin',
platform: 'node',
output: ['types', 'cjs'],
},
{
role: 'backend-plugin-module',
platform: 'node',
output: ['types', 'cjs'],
},
];
const readSchema = z.object({
name: z.string().optional(),
backstage: z
.object({
role: z.string().optional(),
})
.optional(),
});
const detectionSchema = z.object({
name: z.string().optional(),
scripts: z
.object({
start: z.string().optional(),
build: z.string().optional(),
})
.optional(),
publishConfig: z
.object({
main: z.string().optional(),
types: z.string().optional(),
module: z.string().optional(),
})
.optional(),
main: z.string().optional(),
types: z.string().optional(),
module: z.string().optional(),
});
/**
* Utilities for working with Backstage package roles.
*
* @public
*/
export class PackageRoles {
/**
* Get the associated info for a package role.
*/
static getRoleInfo(role: string): PackageRoleInfo {
const roleInfo = packageRoleInfos.find(r => r.role === role);
if (!roleInfo) {
throw new Error(`Unknown package role '${role}'`);
}
return roleInfo;
}
/**
* Given package JSON data, get the package role.
*/
static getRoleFromPackage(pkgJson: unknown): PackageRole | undefined {
const pkg = readSchema.parse(pkgJson);
if (pkg.backstage) {
const { role } = pkg.backstage;
if (!role) {
throw new Error(
`Package ${pkg.name} must specify a role in the "backstage" field`,
);
}
return this.getRoleInfo(role).role;
}
return undefined;
}
/**
* Attempt to detect the role of a package from its package.json.
*/
static detectRoleFromPackage(pkgJson: unknown): PackageRole | undefined {
const pkg = detectionSchema.parse(pkgJson);
if (pkg.scripts?.start?.includes('app:serve')) {
return 'frontend';
}
if (pkg.scripts?.build?.includes('backend:bundle')) {
return 'backend';
}
if (
pkg.name?.includes('plugin-') &&
pkg.name?.includes('-backend-module-')
) {
return 'backend-plugin-module';
}
if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) {
return 'frontend-plugin-module';
}
if (pkg.scripts?.start?.includes('plugin:serve')) {
return 'frontend-plugin';
}
if (pkg.scripts?.start?.includes('backend:dev')) {
return 'backend-plugin';
}
const mainEntry = pkg.publishConfig?.main || pkg.main;
const moduleEntry = pkg.publishConfig?.module || pkg.module;
const typesEntry = pkg.publishConfig?.types || pkg.types;
if (typesEntry) {
if (mainEntry && moduleEntry) {
return 'common-library';
}
if (moduleEntry || mainEntry?.endsWith('.esm.js')) {
return 'web-library';
}
if (mainEntry) {
return 'node-library';
}
} else if (mainEntry) {
return 'cli';
}
return undefined;
}
}
@@ -20,9 +20,4 @@ export type {
PackageOutputType,
PackageRole,
} from './types';
export {
getRoleInfo,
getRoleFromPackage,
findRoleFromCommand,
detectRoleFromPackage,
} from './packageRoles';
export { PackageRoles } from './PackageRoles';
@@ -14,6 +14,11 @@
* limitations under the License.
*/
/**
* Backstage package role, see {@link https://backstage.io/docs/local-dev/cli-build-system#package-roles | docs}.
*
* @public
*/
export type PackageRole =
| 'frontend'
| 'backend'
@@ -26,9 +31,25 @@ export type PackageRole =
| 'backend-plugin'
| 'backend-plugin-module';
/**
* A type of platform that a package can be built for.
*
* @public
*/
export type PackagePlatform = 'node' | 'web' | 'common';
/**
* The type of output that a package can produce.
*
* @public
*/
export type PackageOutputType = 'bundle' | 'types' | 'esm' | 'cjs';
/**
* Information about a package role.
*
* @public
*/
export interface PackageRoleInfo {
role: PackageRole;
platform: PackagePlatform;
@@ -14,15 +14,7 @@
* limitations under the License.
*/
export function chunkArray<T>(array: T[], chunkSize: number): T[][] {
if (chunkSize <= 0) {
return [array];
}
import { execFile as execFileCb } from 'child_process';
import { promisify } from 'util';
const result: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
export const execFile = promisify(execFileCb);
+16
View File
@@ -0,0 +1,16 @@
/*
* 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 {};
+16
View File
@@ -1,5 +1,21 @@
# @backstage/cli
## 0.22.6-next.3
### Patch Changes
- 3156b0d85dc: Internal refactor to move many internal utilities to the new `@backstage/cli-node` package.
- 9bbb00d5b49: Updated dependency `@swc/helpers` to `^0.5.0`.
- Updated dependencies
- @backstage/cli-node@0.1.0-next.0
- @backstage/cli-common@0.1.12
- @backstage/config@1.0.7
- @backstage/config-loader@1.1.9
- @backstage/errors@1.1.5
- @backstage/eslint-plugin@0.1.3-next.0
- @backstage/release-manifests@0.0.9
- @backstage/types@1.0.2
## 0.22.6-next.2
### Patch Changes
+4 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.22.6-next.2",
"version": "0.22.6-next.3",
"publishConfig": {
"access": "public"
},
@@ -31,6 +31,7 @@
},
"dependencies": {
"@backstage/cli-common": "workspace:^",
"@backstage/cli-node": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
"@backstage/errors": "workspace:^",
@@ -55,8 +56,8 @@
"@svgr/plugin-svgo": "6.5.x",
"@svgr/rollup": "6.5.x",
"@svgr/webpack": "6.5.x",
"@swc/core": "^1.3.9",
"@swc/helpers": "^0.4.7",
"@swc/core": "^1.3.46",
"@swc/helpers": "^0.5.0",
"@swc/jest": "^0.2.22",
"@types/jest": "^29.0.0",
"@types/webpack-env": "^1.15.2",
+3 -2
View File
@@ -16,7 +16,8 @@
import { OptionValues } from 'commander';
import { buildPackage, Output } from '../../lib/builder';
import { findRoleFromCommand, getRoleInfo } from '../../lib/role';
import { findRoleFromCommand } from '../../lib/role';
import { PackageRoles } from '@backstage/cli-node';
import { paths } from '../../lib/paths';
import { buildFrontend } from './buildFrontend';
import { buildBackend } from './buildBackend';
@@ -47,7 +48,7 @@ export async function command(opts: OptionValues): Promise<void> {
});
}
const roleInfo = getRoleInfo(role);
const roleInfo = PackageRoles.getRoleInfo(role);
const outputs = new Set<Output>();
+1 -1
View File
@@ -19,7 +19,7 @@ import { ESLint } from 'eslint';
import { join as joinPath, basename } from 'path';
import fs from 'fs-extra';
import { isChildPath } from '@backstage/cli-common';
import { PackageGraph } from '../lib/monorepo';
import { PackageGraph } from '@backstage/cli-node';
function isTestPath(filePath: string) {
if (!isChildPath(joinPath(paths.targetDir, 'src'), filePath)) {
@@ -16,7 +16,7 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { ExtendedPackageJSON, PackageGraph } from '../../lib/monorepo';
import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node';
function trimRelative(path: string): string {
if (path.startsWith('./')) {
@@ -94,7 +94,7 @@ export async function command() {
newPackageJson = Object.fromEntries(
newPkgEntries,
) as ExtendedPackageJSON;
) as BackstagePackageJson;
changed = true;
}
@@ -16,7 +16,7 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { PackageGraph } from '../../lib/monorepo';
import { PackageGraph } from '@backstage/cli-node';
import { runPlain } from '../../lib/run';
const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`;
@@ -17,8 +17,8 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { getPackages } from '@manypkg/get-packages';
import { PackageRoles } from '@backstage/cli-node';
import { paths } from '../../lib/paths';
import { getRoleFromPackage, detectRoleFromPackage } from '../../lib/role';
export default async () => {
const { packages } = await getPackages(paths.targetDir);
@@ -26,12 +26,12 @@ export default async () => {
await Promise.all(
packages.map(async ({ dir, packageJson: pkg }) => {
const { name } = pkg;
const existingRole = getRoleFromPackage(pkg);
const existingRole = PackageRoles.getRoleFromPackage(pkg);
if (existingRole) {
return;
}
const detectedRole = detectRoleFromPackage(pkg);
const detectedRole = PackageRoles.detectRoleFromPackage(pkg);
if (!detectedRole) {
console.error(`No role detected for package ${name}`);
return;
@@ -16,8 +16,7 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { PackageGraph } from '../../lib/monorepo';
import { getRoleFromPackage, getRoleInfo, PackageRole } from '../../lib/role';
import { PackageGraph, PackageRoles, PackageRole } from '@backstage/cli-node';
const configArgPattern = /--config[=\s][^\s$]+/;
@@ -28,12 +27,12 @@ export async function command() {
await Promise.all(
packages.map(async ({ dir, packageJson }) => {
const role = getRoleFromPackage(packageJson);
const role = PackageRoles.getRoleFromPackage(packageJson);
if (!role) {
return;
}
const roleInfo = getRoleInfo(role);
const roleInfo = PackageRoles.getRoleInfo(role);
const hasStart = !noStartRoles.includes(role);
const needsPack = !(roleInfo.output.includes('bundle') || role === 'cli');
const scripts = packageJson.scripts ?? {};
@@ -16,8 +16,7 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { PackageGraph } from '../../lib/monorepo';
import { getRoleFromPackage } from '../../lib/role';
import { PackageGraph, PackageRoles } from '@backstage/cli-node';
const REACT_ROUTER_DEPS = ['react-router', 'react-router-dom'];
const REACT_ROUTER_RANGE = '6.0.0-beta.0 || ^6.3.0';
@@ -27,7 +26,7 @@ export async function command() {
await Promise.all(
packages.map(async ({ dir, packageJson }) => {
const role = getRoleFromPackage(packageJson);
const role = PackageRoles.getRoleFromPackage(packageJson);
if (role === 'frontend') {
console.log(`Skipping ${packageJson.name}`);
return;
+1 -1
View File
@@ -19,10 +19,10 @@ import fs from 'fs-extra';
import { join as joinPath } from 'path';
import { OptionValues } from 'commander';
import { FactoryRegistry } from '../../lib/new/FactoryRegistry';
import { isMonoRepo } from '@backstage/cli-node';
import { paths } from '../../lib/paths';
import { assertError } from '@backstage/errors';
import { Task } from '../../lib/tasks';
import { isMonoRepo } from '../../lib/monorepo/isMonoRepo';
function parseOptions(optionStrings: string[]): Record<string, string> {
const options: Record<string, string> = {};
+10 -7
View File
@@ -18,11 +18,13 @@ import chalk from 'chalk';
import { Command, OptionValues } from 'commander';
import { relative as relativePath } from 'path';
import { buildPackages, getOutputsForRole } from '../../lib/builder';
import { PackageGraph } from '../../lib/monorepo';
import { ExtendedPackage } from '../../lib/monorepo/PackageGraph';
import { runParallelWorkers } from '../../lib/parallel';
import { paths } from '../../lib/paths';
import { detectRoleFromPackage } from '../../lib/role';
import {
PackageGraph,
BackstagePackage,
PackageRoles,
} from '@backstage/cli-node';
import { runParallelWorkers } from '../../lib/parallel';
import { buildFrontend } from '../build/buildFrontend';
import { buildBackend } from '../build/buildBackend';
@@ -93,14 +95,15 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
packages = Array.from(withDevDependents).map(name => graph.get(name)!);
}
const apps = new Array<ExtendedPackage>();
const backends = new Array<ExtendedPackage>();
const apps = new Array<BackstagePackage>();
const backends = new Array<BackstagePackage>();
const parseBuildScript = createScriptOptionsParser(cmd, ['package', 'build']);
const options = packages.flatMap(pkg => {
const role =
pkg.packageJson.backstage?.role ?? detectRoleFromPackage(pkg.packageJson);
pkg.packageJson.backstage?.role ??
PackageRoles.detectRoleFromPackage(pkg.packageJson);
if (!role) {
console.warn(`Ignored ${pkg.packageJson.name} because it has no role`);
return [];
+1 -1
View File
@@ -18,7 +18,7 @@ import { execFile as execFileCb } from 'child_process';
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { promisify } from 'util';
import { PackageGraph } from '../../lib/monorepo';
import { PackageGraph } from '@backstage/cli-node';
import { paths } from '../../lib/paths';
const execFile = promisify(execFileCb);
+3 -3
View File
@@ -17,11 +17,11 @@
import chalk from 'chalk';
import { OptionValues } from 'commander';
import { relative as relativePath } from 'path';
import { PackageGraph, ExtendedPackageJSON } from '../../lib/monorepo';
import { runWorkerQueueThreads } from '../../lib/parallel';
import { PackageGraph, BackstagePackageJson } from '@backstage/cli-node';
import { paths } from '../../lib/paths';
import { runWorkerQueueThreads } from '../../lib/parallel';
function depCount(pkg: ExtendedPackageJSON) {
function depCount(pkg: BackstagePackageJson) {
const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0;
const devDeps = pkg.devDependencies
? Object.keys(pkg.devDependencies).length
@@ -18,8 +18,8 @@ import chalk from 'chalk';
import { ESLint } from 'eslint';
import { OptionValues } from 'commander';
import { join as joinPath, relative as relativePath } from 'path';
import { PackageGraph } from '@backstage/cli-node';
import { paths } from '../../lib/paths';
import { PackageGraph } from '../../lib/monorepo';
export async function command(opts: OptionValues) {
const packages = await PackageGraph.listTargetPackages();
+1 -1
View File
@@ -16,7 +16,7 @@
import os from 'os';
import { Command, OptionValues } from 'commander';
import { PackageGraph } from '../../lib/monorepo';
import { PackageGraph } from '@backstage/cli-node';
import { paths } from '../../lib/paths';
import { runCheck } from '../../lib/run';
@@ -19,10 +19,10 @@ import chalk from 'chalk';
import uniq from 'lodash/uniq';
import { serveBundle } from '../../lib/bundler';
import { loadCliConfig } from '../../lib/config';
import { paths } from '../../lib/paths';
import { PackageGraph } from '@backstage/cli-node';
import { Lockfile } from '../../lib/versioning';
import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint';
import { PackageGraph } from '../../lib/monorepo';
import { paths } from '../../lib/paths';
interface StartAppOptions {
verifyVersions?: boolean;
+1 -1
View File
@@ -39,7 +39,7 @@ import {
ReleaseManifest,
} from '@backstage/release-manifests';
import 'global-agent/bootstrap';
import { PackageGraph } from '../../lib/monorepo';
import { PackageGraph } from '@backstage/cli-node';
const DEP_TYPES = [
'dependencies',

Some files were not shown because too many files have changed in this diff Show More