Merge branch 'master' of github.com:backstage/backstage into scaffolder-examples

This commit is contained in:
Brian Fletcher
2022-12-24 12:42:56 +00:00
68 changed files with 2495 additions and 327 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Fixed an issue where the CLI would fail to function when there was a mix of workspace and non-workspace versions of the same package in `yarn.lock` when using Yarn 3.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
The frontend serve task now filters out allowed package duplicates during its package check, just like `versions:bump` and `versions:check`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Fixed bug in `EntityTagPicker` that filtered on unavailable tags for the selected kind.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': patch
---
Move some dependencies as `peerDependencies` because we need to always use same version as in `api-extractor`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
add `onChange` handler to`Stepper` component
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-code-coverage-backend': patch
---
`RouterOptions` now accepts an optional `catalogApi` argument, allowing custom catalog clients to be used. This is leveraged in the local standalone development runner to pass in a catalog client that returns fake data.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-module-elasticsearch': patch
---
Fixed a bug that could cause an indexing process to silently fail, timeout, and accumulate stale indices.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-module-elasticsearch': patch
---
Fixed a bug that could cause the backstage backend to unexpectedly terminate when client errors were encountered during the indexing process.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-module-elasticsearch': patch
---
Improved index throughput by optimizing when and how many documents were made available to the bulk client.
+37
View File
@@ -0,0 +1,37 @@
---
'@backstage/plugin-scaffolder': minor
'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch
'@backstage/plugin-scaffolder-backend-module-rails': patch
'@backstage/plugin-scaffolder-backend-module-yeoman': patch
'@backstage/plugin-scaffolder-backend': patch
---
Added `catalogFilter` field to OwnerPicker and EntityPicker components to support filtering options by any field(s) of an entity.
The `allowedKinds` field has been deprecated. Use `catalogFilter` instead. This field allows users to specify a filter on the shape of [EntityFilterQuery](https://github.com/backstage/backstage/blob/774c42003782121d3d6b2aa5f2865d53370c160e/packages/catalog-client/src/types/api.ts#L74), which can be passed into the CatalogClient. See examples below:
- Get all entities of kind `Group`
```yaml
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
catalogFilter:
- kind: Group
```
- Get entities of kind `Group` and spec.type `team`
```yaml
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
catalogFilter:
- kind: Group
spec.type: team
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-ilert': patch
---
fixed error on service page not showing if historical uptime was disabled on a service
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
The `repo test`, `repo lint`, and `repo build` commands will now analyze `yarn.lock` for dependency changes when searching for changed packages. This allows you to use the `--since <ref>` flag even if you have `yarn.lock` changes.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-explore': patch
---
Makes the `GroupsDiagram` not grown on screen on its own.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Workspace ranges are no longer considered invalid by version commands.
+3 -3
View File
@@ -8,11 +8,11 @@ x-uffizzi:
services:
backstage:
image: '${BACKSTAGE_IMAGE}'
environment:
POSTGRES_HOST: localhost
POSTGRES_HOST: db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: example
GITHUB_TOKEN: abc
NODE_ENV: production
deploy:
resources:
@@ -21,7 +21,7 @@ services:
entrypoint: '/bin/sh'
command:
- '-c'
- "APP_CONFIG_app_baseUrl=$$UFFIZZI_URL APP_CONFIG_backend_baseUrl=$$UFFIZZI_URL APP_CONFIG_auth_environment='production' node packages/backend --config app-config.yaml --config app-config.production.yaml"
- "APP_CONFIG_app_baseUrl=$$UFFIZZI_URL APP_CONFIG_backend_baseUrl=$$UFFIZZI_URL APP_CONFIG_auth_environment='production' node packages/backend --config app-config.yaml"
db:
image: postgres
@@ -2,9 +2,258 @@ app:
title: Backstage Uffizzi Environment
baseUrl: ${UFFIZZI_URL}
organization:
name: My Company
backend:
baseUrl: ${UFFIZZI_URL}
auth:
keys:
# random mock key for uffizi deployments
- secret: 5TXvdjVZFxF7qf9K5RAYRDoGrLzJooqa
listen:
port: 7007
database:
client: better-sqlite3
connection: ':memory:'
cache:
store: memory
cors:
origin: ${UFFIZZI_URL}
methods: [GET, POST, PUT, DELETE]
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
csp:
connect-src: ["'self'", 'http:', 'https:']
# Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference
# Default Helmet Content-Security-Policy values can be removed by setting the key to false
auth:
environment: production
providers: {}
catalog:
locations:
- type: url
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml
proxy:
'/circleci/api':
target: https://circleci.com/api/v1.1
headers:
Circle-Token: ${CIRCLECI_AUTH_TOKEN}
'/jenkins/api':
target: http://localhost:8080
headers:
Authorization: ${JENKINS_BASIC_AUTH_HEADER}
'/travisci/api':
target: https://api.travis-ci.com
changeOrigin: true
headers:
Authorization: ${TRAVISCI_AUTH_TOKEN}
travis-api-version: '3'
'/newrelic/apm/api':
target: https://api.newrelic.com/v2
headers:
X-Api-Key: ${NEW_RELIC_REST_API_KEY}
'/newrelic/api':
target: https://api.newrelic.com
headers:
X-Api-Key: ${NEW_RELIC_USER_KEY}
'/pagerduty':
target: https://api.pagerduty.com
headers:
Authorization: Token token=${PAGERDUTY_TOKEN}
'/buildkite/api':
target: https://api.buildkite.com/v2/
headers:
Authorization: ${BUILDKITE_TOKEN}
'/sentry/api':
target: https://sentry.io/api/
allowedMethods: ['GET']
headers:
Authorization: ${SENTRY_TOKEN}
'/ilert':
target: https://api.ilert.com
allowedMethods: ['GET', 'POST', 'PUT']
allowedHeaders: ['Authorization']
headers:
Authorization: ${ILERT_AUTH_HEADER}
'/airflow':
target: https://your.airflow.instance.com/api/v1
headers:
Authorization: ${AIRFLOW_BASIC_AUTH_HEADER}
'/gocd':
target: https://your.gocd.instance.com/go/api
allowedMethods: ['GET']
allowedHeaders: ['Authorization']
headers:
Authorization: Basic ${GOCD_AUTH_CREDENTIALS}
'/dynatrace':
target: https://your.dynatrace.instance.com/api/v2
headers:
Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}'
techdocs:
builder: 'local' # Alternatives - 'external'
generator:
runIn: 'docker'
# dockerImage: my-org/techdocs # use a custom docker image
# pullImage: true # or false to disable automatic pulling of image (e.g. if custom docker login is required)
publisher:
type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift'. Read documentation for using alternatives.
dynatrace:
baseUrl: https://your.dynatrace.instance.com
# Score-cards sample configuration.
scorecards:
jsonDataUrl: https://raw.githubusercontent.com/Oriflame/backstage-plugins/main/plugins/score-card/sample-data/
wikiLinkTemplate: https://link-to-wiki/{id}
sentry:
organization: my-company
rollbar:
organization: my-company
# NOTE: The rollbar-backend & accountToken key may be deprecated in the future (replaced by a proxy config)
accountToken: my-rollbar-account-token
lighthouse:
baseUrl: http://localhost:3003
kubernetes:
serviceLocatorMethod:
type: 'multiTenant'
clusterLocatorMethods:
- type: 'config'
clusters: []
kafka:
clientId: backstage
clusters:
- name: cluster
dashboardUrl: https://akhq.io/
brokers:
- localhost:9092
allure:
baseUrl: http://localhost:5050/allure-docker-service
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
### Example for how to add your GitHub Enterprise instance using the API:
# - host: ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token: ${GHE_TOKEN}
### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional):
# - host: ghe.example.net
# rawBaseUrl: https://ghe.example.net/raw
# token: ${GHE_TOKEN}
gitlab:
- host: gitlab.com
token: ${GITLAB_TOKEN}
### Example for how to add a bitbucket cloud integration
# bitbucketCloud:
# - username: ${BITBUCKET_USERNAME}
# appPassword: ${BITBUCKET_APP_PASSWORD}
### Example for how to add your bitbucket server instance using the API:
# - host: server.bitbucket.com
# apiBaseUrl: server.bitbucket.com
# username: ${BITBUCKET_SERVER_USERNAME}
# appPassword: ${BITBUCKET_SERVER_APP_PASSWORD}
azure:
- host: dev.azure.com
token: ${AZURE_TOKEN}
# googleGcs:
# clientEmail: 'example@example.com'
# privateKey: ${GCS_PRIVATE_KEY}
awsS3:
- host: amazonaws.com
accessKeyId: ${AWS_ACCESS_KEY_ID}
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
costInsights:
engineerCost: 200000
engineerThreshold: 0.5
products:
computeEngine:
name: Compute Engine
icon: compute
cloudDataflow:
name: Cloud Dataflow
icon: data
cloudStorage:
name: Cloud Storage
icon: storage
bigQuery:
name: BigQuery
icon: search
events:
name: Events
icon: data
metrics:
DAU:
name: Daily Active Users
default: true
MSC:
name: Monthly Subscribers
currencies:
engineers:
label: 'Engineers 🛠'
unit: 'engineer'
usd:
label: 'US Dollars 💵'
kind: 'USD'
unit: 'dollar'
prefix: '$'
rate: 1
carbonOffsetTons:
label: 'Carbon Offset Tons ♻️⚖️s'
kind: 'CARBON_OFFSET_TONS'
unit: 'carbon offset ton'
rate: 3.5
beers:
label: 'Beers 🍺'
kind: 'BEERS'
unit: 'beer'
rate: 4.5
pintsIceCream:
label: 'Pints of Ice Cream 🍦'
kind: 'PINTS_OF_ICE_CREAM'
unit: 'ice cream pint'
rate: 5.5
pagerduty:
eventsBaseUrl: 'https://events.pagerduty.com/v2'
jenkins:
instances:
- name: default
baseUrl: https://jenkins.example.com
username: backstage-bot
apiKey: 123456789abcdef0123456789abcedf012
azureDevOps:
host: dev.azure.com
token: my-token
organization: my-company
apacheAirflow:
baseUrl: https://your.airflow.instance.com
gocd:
baseUrl: https://your.gocd.instance.com
permission:
enabled: true
-22
View File
@@ -185,21 +185,10 @@ jobs:
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
- name: check for yarn.lock changes
id: yarn-lock
run: git diff --quiet origin/master HEAD -- yarn.lock
continue-on-error: true
- name: lint changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
run: yarn backstage-cli repo lint --since origin/master
- name: lint all packages
if: ${{ steps.yarn-lock.outcome == 'failure' }}
run: yarn backstage-cli repo lint
- name: test changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
run: yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=1300M --since origin/master
env:
BACKSTAGE_TEST_DISABLE_DOCKER: 1
@@ -207,17 +196,6 @@ jobs:
BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }}
BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored
- name: test all packages (and upload coverage)
if: ${{ steps.yarn-lock.outcome == 'failure' }}
run: |
yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=800M --coverage
bash <(curl -s https://codecov.io/bash) -N $(git rev-parse FETCH_HEAD)
env:
BACKSTAGE_TEST_DISABLE_DOCKER: 1
BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }}
BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }}
BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored
- name: ensure clean working directory
run: |
if files=$(git ls-files --exclude-standard --others --modified) && [[ -z "$files" ]]; then
+18 -16
View File
@@ -8,6 +8,9 @@ on:
jobs:
build-backstage:
env:
NODE_OPTIONS: --max-old-space-size=4096
UFFIZZI_URL: https://uffizzi.com
name: Build PR image
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
@@ -16,8 +19,6 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v3
with:
path: backstage
- name: setup-node
uses: actions/setup-node@v3
@@ -25,21 +26,22 @@ jobs:
node-version: 16.x
registry-url: https://registry.npmjs.org/
- name: backstage create-app | delete if already exists
run: |
rm -rf ./example-app
npx @backstage/create-app
env:
BACKSTAGE_APP_NAME: example-app
- name: yarn build
run: |
yarn build:all
working-directory: ./example-app
- name: yarn install
uses: backstage/actions/yarn-install@v0.5.12
with:
cache-prefix: linux-v16
- name: Use Uffizzi's backstage app config
run: |
cp ./backstage/.github/uffizzi/uffizzi.production.app-config.yaml ./example-app/app-config.production.yaml;
cp -f ./.github/uffizzi/uffizzi.production.app-config.yaml ./app-config.yaml
- name: typescript build
run: |
yarn tsc
- name: backstage build
run: |
yarn workspace example-backend build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -58,8 +60,8 @@ jobs:
- name: Build Image
uses: docker/build-push-action@v2
with:
context: example-app
file: example-app/packages/backend/Dockerfile
context: .
file: packages/backend/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true
@@ -251,6 +251,23 @@ the entity belongs to the `"default"` namespace.
Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities,
i.e. not Backstage specific but the same as in Kubernetes.
### `uid` [output]
Each entity gets an automatically generated globally unique ID when it first
enters the database. This field is not meant to be specified as input data, but
is rater created by the database engine itself when producing the output entity.
Note that `uid` values are _not_ to be seen as stable, and should _not_ be used
as external references to an entity. The `uid` can change over time even when a
human observer might think that it wouldn't. As one of many examples,
unregistering and re-registering the exact same file will result in a different
`uid` value even though everything else is the same. Therefore there is very
little, if any, reason to read or use this field externally.
If you want to refer to an entity by some form of an identifier, you should
always use [string-form entity reference](references.md#string-references)
instead.
### `title` [optional]
A display name of the entity, to be presented in user interfaces instead of the
@@ -44,8 +44,8 @@ spec:
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
catalogFilter:
kind: Group
- title: Choose a location
required:
- repoUrl
@@ -165,11 +165,11 @@ this:
"firstName": {
"ui:autofocus": true,
"ui:emptyValue": "",
"ui:autocomplete": "family-name"
"ui:autocomplete": "given-name"
},
"lastName": {
"ui:emptyValue": "",
"ui:autocomplete": "given-name"
"ui:autocomplete": "family-name"
},
"nicknames": {
"ui:options":{
@@ -211,12 +211,12 @@ spec:
default: Chuck
ui:autofocus: true
ui:emptyValue: ''
ui:autocomplete: family-name
ui:autocomplete: given-name
lastName:
type: string
title: Last name
ui:emptyValue: ''
ui:autocomplete: given-name
ui:autocomplete: family-name
nicknames:
type: array
items:
@@ -459,7 +459,7 @@ an owner for them. Ideally, users should be able to select an owner when they go
through the scaffolder form from the users and groups already known to
Backstage. The `OwnerPicker` is a custom field that generates a searchable list
of groups and/or users already in the catalog to pick an owner from. You can
specify which of the two kinds are listed in the `allowedKinds` option:
specify which of the two kinds (or both) are listed in the `catalogFilter.kind` option:
```yaml
owner:
@@ -468,8 +468,8 @@ owner:
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
catalogFilter:
kind: [Group, User]
```
## `spec.steps` - `Action[]`
+4 -4
View File
@@ -4,9 +4,8 @@
"license": "Apache-2.0",
"private": true,
"scripts": {
"examples": "docusaurus-examples",
"start": "docusaurus start",
"build": "docusaurus build",
"start": "node scripts/pre-build.js && docusaurus start",
"build": "node scripts/pre-build.js && docusaurus build",
"prettier:check": "prettier --check .",
"publish-gh-pages": "docusaurus-publish",
"write-translations": "docusaurus-write-translations",
@@ -20,7 +19,8 @@
"devDependencies": {
"@spotify/prettier-config": "^14.0.0",
"js-yaml": "^4.1.0",
"prettier": "^2.6.2"
"prettier": "^2.6.2",
"replace": "^1.2.2"
},
"prettier": "@spotify/prettier-config",
"dependencies": {
+49
View File
@@ -0,0 +1,49 @@
/*
* 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.
*/
const replace = require('replace');
const { existsSync, writeFileSync, mkdirSync } = require('fs');
const path = require('path');
const PLACEHOLDER = `---
id: "index"
title: "Package Index"
description: "Index of all Backstage Packages"
---
Run \`yarn build:api-docs\` to generate the API docs.
`;
async function main() {
const referencesDir = '../docs/reference';
if (existsSync(referencesDir)) {
console.log('Removing HTML comments from docs/reference folder');
replace({
regex: '<!--(.*?)-->',
replacement: '',
paths: [referencesDir],
recursive: true,
silent: false,
});
} else {
mkdirSync(referencesDir);
writeFileSync(path.join(referencesDir, 'index.md'), PLACEHOLDER);
}
}
main().catch(error => {
console.error(error.stack);
process.exit(1);
});
+366 -1
View File
@@ -1 +1,366 @@
{}
{
"releases": {
"Release Notes": [
"releases/v1.8.0",
"releases/v1.7.0",
"releases/v1.6.0",
"releases/v1.5.0",
"releases/v1.4.0",
"releases/v1.3.0",
"releases/v1.2.0",
"releases/v1.1.0",
"releases/v1.0.0"
]
},
"docs": {
"Overview": [
"overview/what-is-backstage",
"overview/architecture-overview",
"overview/roadmap",
"overview/vision",
"overview/background",
"overview/adopting",
"overview/versioning-policy",
"overview/threat-model",
"overview/support",
"overview/glossary",
"overview/logos"
],
"Getting Started": [
"getting-started/index",
"getting-started/configuration",
"getting-started/create-an-app",
"getting-started/running-backstage-locally",
{
"type": "category",
"label": "App configuration",
"items": [
"getting-started/configure-app-with-plugins",
"getting-started/app-custom-theme",
"getting-started/homepage"
]
},
"getting-started/keeping-backstage-updated",
"getting-started/concepts",
"getting-started/contributors",
"getting-started/project-structure"
],
"Local Development": [
{
"type": "category",
"label": "CLI",
"items": [
"local-dev/cli-overview",
"local-dev/cli-build-system",
"local-dev/cli-commands"
]
},
"local-dev/linking-local-packages"
],
"Core Features": [
{
"type": "category",
"label": "Software Catalog",
"items": [
"features/software-catalog/software-catalog-overview",
"features/software-catalog/life-of-an-entity",
"features/software-catalog/configuration",
"features/software-catalog/system-model",
"features/software-catalog/descriptor-format",
"features/software-catalog/references",
"features/software-catalog/well-known-annotations",
"features/software-catalog/well-known-relations",
"features/software-catalog/well-known-statuses",
"features/software-catalog/extending-the-model",
"features/software-catalog/external-integrations",
"features/software-catalog/catalog-customization",
"features/software-catalog/software-catalog-api"
]
},
{
"type": "category",
"label": "Kubernetes",
"items": [
"features/kubernetes/overview",
"features/kubernetes/installation",
"features/kubernetes/configuration",
"features/kubernetes/authentication",
"features/kubernetes/troubleshooting"
]
},
{
"type": "category",
"label": "Software Templates",
"items": [
"features/software-templates/software-templates-index",
"features/software-templates/configuration",
"features/software-templates/adding-templates",
"features/software-templates/writing-templates",
"features/software-templates/input-examples",
"features/software-templates/builtin-actions",
"features/software-templates/writing-custom-actions",
"features/software-templates/writing-custom-field-extensions",
"features/software-templates/writing-custom-step-layouts",
"features/software-templates/migrating-from-v1beta2-to-v1beta3"
]
},
{
"type": "category",
"label": "Backstage Search",
"items": [
"features/search/search-overview",
"features/search/getting-started",
"features/search/concepts",
"features/search/architecture",
"features/search/search-engines",
"features/search/how-to-guides"
]
},
{
"type": "category",
"label": "TechDocs",
"items": [
"features/techdocs/techdocs-overview",
"features/techdocs/getting-started",
"features/techdocs/concepts",
"features/techdocs/addons",
"features/techdocs/architecture",
"features/techdocs/creating-and-publishing",
"features/techdocs/configuration",
"features/techdocs/using-cloud-storage",
"features/techdocs/configuring-ci-cd",
"features/techdocs/cli",
"features/techdocs/how-to-guides",
"features/techdocs/troubleshooting",
"features/techdocs/faqs"
]
}
],
"Integrations": [
"integrations/index",
{
"type": "category",
"label": "AWS S3",
"items": [
"integrations/aws-s3/locations",
"integrations/aws-s3/discovery"
]
},
{
"type": "category",
"label": "Azure",
"items": [
"integrations/azure/locations",
"integrations/azure/discovery",
"integrations/azure/org"
]
},
{
"type": "category",
"label": "Bitbucket Cloud",
"items": [
"integrations/bitbucketCloud/locations",
"integrations/bitbucketCloud/discovery"
]
},
{
"type": "category",
"label": "Bitbucket Server",
"items": [
"integrations/bitbucketServer/locations",
"integrations/bitbucketServer/discovery"
]
},
{
"type": "category",
"label": "Datadog",
"items": ["integrations/datadog-rum/installation"]
},
{
"type": "category",
"label": "Gerrit",
"items": [
"integrations/gerrit/locations",
"integrations/gerrit/discovery"
]
},
{
"type": "category",
"label": "GitHub",
"items": [
"integrations/github/locations",
"integrations/github/discovery",
"integrations/github/org",
"integrations/github/github-apps"
]
},
{
"type": "category",
"label": "GitLab",
"items": [
"integrations/gitlab/locations",
"integrations/gitlab/discovery"
]
},
{
"type": "category",
"label": "Gitea",
"items": ["integrations/gitea/locations"]
},
{
"type": "category",
"label": "Google GCS",
"items": ["integrations/google-cloud-storage/locations"]
},
{
"type": "category",
"label": "LDAP",
"items": ["integrations/ldap/org"]
}
],
"Plugins": [
"plugins/index",
"plugins/existing-plugins",
"plugins/create-a-plugin",
"plugins/plugin-development",
"plugins/structure-of-a-plugin",
"plugins/integrating-plugin-into-software-catalog",
"plugins/integrating-search-into-plugins",
"plugins/composability",
"plugins/customization",
"plugins/analytics",
"plugins/feature-flags",
{
"type": "category",
"label": "Backends and APIs",
"items": [
"plugins/proxying",
"plugins/backend-plugin",
"plugins/call-existing-api",
"plugins/url-reader",
"plugins/new-backend-system"
]
},
{
"type": "category",
"label": "Testing",
"items": ["plugins/testing"]
},
{
"type": "category",
"label": "Publishing",
"items": [
"plugins/publish-private",
"plugins/add-to-marketplace",
"plugins/observability"
]
}
],
"Configuration": [
"conf/index",
"conf/reading",
"conf/writing",
"conf/defining"
],
"Auth and identity": [
"auth/index",
{
"type": "category",
"label": "Included providers",
"items": [
"auth/auth0/provider",
"auth/atlassian/provider",
"auth/bitbucket/provider",
"auth/microsoft/provider",
"auth/github/provider",
"auth/gitlab/provider",
"auth/google/provider",
"auth/google/gcp-iap-auth",
"auth/okta/provider",
"auth/onelogin/provider",
"auth/oauth2-proxy/provider"
]
},
"auth/identity-resolver",
"auth/oauth",
"auth/oidc",
"auth/add-auth-provider",
"auth/service-to-service-auth",
"auth/troubleshooting",
"auth/glossary"
],
"Permissions": [
"permissions/overview",
"permissions/concepts",
"permissions/getting-started",
"permissions/writing-a-policy",
"permissions/frontend-integration",
"permissions/custom-rules",
{
"type": "category",
"label": "Tutorial: using Permissions in your plugin",
"items": [
"permissions/plugin-authors/01-setup",
"permissions/plugin-authors/02-adding-a-basic-permission-check",
"permissions/plugin-authors/03-adding-a-resource-permission-check",
"permissions/plugin-authors/04-authorizing-access-to-paginated-data",
"permissions/plugin-authors/05-frontend-authorization"
]
}
],
"Deployment": [
"deployment/index",
"deployment/scaling",
"deployment/docker",
"deployment/k8s",
"deployment/heroku"
],
"Designing for Backstage": [
"dls/design",
"dls/component-design-guidelines",
"dls/contributing-to-storybook",
"dls/figma"
],
"API Reference": [
{
"type": "category",
"label": "Guides",
"items": ["api/utility-apis"]
},
{
"type": "category",
"label": "API Reference",
"items": ["reference/index"]
},
"api/deprecations"
],
"Tutorials": [
"tutorials/journey",
"tutorials/quickstart-app-plugin",
"tutorials/react-router-stable-migration",
"tutorials/package-role-migration",
"tutorials/migrating-away-from-core",
"tutorials/configuring-plugin-databases",
"tutorials/switching-sqlite-postgres",
"tutorials/using-backstage-proxy-within-plugin",
"tutorials/yarn-migration"
],
"Architecture Decision Records (ADRs)": [
"architecture-decisions/adrs-overview",
"architecture-decisions/adrs-adr001",
"architecture-decisions/adrs-adr002",
"architecture-decisions/adrs-adr003",
"architecture-decisions/adrs-adr004",
"architecture-decisions/adrs-adr005",
"architecture-decisions/adrs-adr006",
"architecture-decisions/adrs-adr007",
"architecture-decisions/adrs-adr008",
"architecture-decisions/adrs-adr009",
"architecture-decisions/adrs-adr010",
"architecture-decisions/adrs-adr011",
"architecture-decisions/adrs-adr012",
"architecture-decisions/adrs-adr013"
],
"FAQ": ["FAQ"]
}
}
+126 -2
View File
@@ -3573,6 +3573,7 @@ __metadata:
prettier: ^2.6.2
react: ^17.0.2
react-dom: ^17.0.2
replace: ^1.2.2
languageName: unknown
linkType: soft
@@ -3827,6 +3828,13 @@ __metadata:
languageName: node
linkType: hard
"camelcase@npm:^5.0.0":
version: 5.3.1
resolution: "camelcase@npm:5.3.1"
checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b
languageName: node
linkType: hard
"camelcase@npm:^6.2.0":
version: 6.3.0
resolution: "camelcase@npm:6.3.0"
@@ -3860,7 +3868,7 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^2.0.0":
"chalk@npm:2.4.2, chalk@npm:^2.0.0":
version: 2.4.2
resolution: "chalk@npm:2.4.2"
dependencies:
@@ -4021,6 +4029,17 @@ __metadata:
languageName: node
linkType: hard
"cliui@npm:^6.0.0":
version: 6.0.0
resolution: "cliui@npm:6.0.0"
dependencies:
string-width: ^4.2.0
strip-ansi: ^6.0.0
wrap-ansi: ^6.2.0
checksum: 4fcfd26d292c9f00238117f39fc797608292ae36bac2168cfee4c85923817d0607fe21b3329a8621e01aedf512c99b7eaa60e363a671ffd378df6649fb48ae42
languageName: node
linkType: hard
"clone-deep@npm:^4.0.1":
version: 4.0.1
resolution: "clone-deep@npm:4.0.1"
@@ -4597,6 +4616,13 @@ __metadata:
languageName: node
linkType: hard
"decamelize@npm:^1.2.0":
version: 1.2.0
resolution: "decamelize@npm:1.2.0"
checksum: ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa
languageName: node
linkType: hard
"decompress-response@npm:^3.3.0":
version: 3.3.0
resolution: "decompress-response@npm:3.3.0"
@@ -5355,7 +5381,7 @@ __metadata:
languageName: node
linkType: hard
"find-up@npm:^4.0.0":
"find-up@npm:^4.0.0, find-up@npm:^4.1.0":
version: 4.1.0
resolution: "find-up@npm:4.1.0"
dependencies:
@@ -5544,6 +5570,13 @@ __metadata:
languageName: node
linkType: hard
"get-caller-file@npm:^2.0.1":
version: 2.0.5
resolution: "get-caller-file@npm:2.0.5"
checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9
languageName: node
linkType: hard
"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1":
version: 1.1.3
resolution: "get-intrinsic@npm:1.1.3"
@@ -7164,6 +7197,15 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:3.0.5":
version: 3.0.5
resolution: "minimatch@npm:3.0.5"
dependencies:
brace-expansion: ^1.1.7
checksum: a3b84b426eafca947741b864502cee02860c4e7b145de11ad98775cfcf3066fef422583bc0ffce0952ddf4750c1ccf4220b1556430d4ce10139f66247d87d69e
languageName: node
linkType: hard
"minimatch@npm:3.1.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1":
version: 3.1.2
resolution: "minimatch@npm:3.1.2"
@@ -8982,6 +9024,27 @@ __metadata:
languageName: node
linkType: hard
"replace@npm:^1.2.2":
version: 1.2.2
resolution: "replace@npm:1.2.2"
dependencies:
chalk: 2.4.2
minimatch: 3.0.5
yargs: ^15.3.1
bin:
replace: bin/replace.js
search: bin/search.js
checksum: 1d69f43937a5fdf9dea278e78d6f3b51c1889ba5135bd201918bbda6330684adf8276e8e90e1c021034420dd4df239e51c23ca40752cb9bc6180c153d6d46a37
languageName: node
linkType: hard
"require-directory@npm:^2.1.1":
version: 2.1.1
resolution: "require-directory@npm:2.1.1"
checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80
languageName: node
linkType: hard
"require-from-string@npm:^2.0.2":
version: 2.0.2
resolution: "require-from-string@npm:2.0.2"
@@ -8996,6 +9059,13 @@ __metadata:
languageName: node
linkType: hard
"require-main-filename@npm:^2.0.0":
version: 2.0.0
resolution: "require-main-filename@npm:2.0.0"
checksum: e9e294695fea08b076457e9ddff854e81bffbe248ed34c1eec348b7abbd22a0d02e8d75506559e2265e96978f3c4720bd77a6dad84755de8162b357eb6c778c7
languageName: node
linkType: hard
"requires-port@npm:^1.0.0":
version: 1.0.0
resolution: "requires-port@npm:1.0.0"
@@ -10623,6 +10693,13 @@ __metadata:
languageName: node
linkType: hard
"which-module@npm:^2.0.0":
version: 2.0.0
resolution: "which-module@npm:2.0.0"
checksum: 809f7fd3dfcb2cdbe0180b60d68100c88785084f8f9492b0998c051d7a8efe56784492609d3f09ac161635b78ea29219eb1418a98c15ce87d085bce905705c9c
languageName: node
linkType: hard
"which@npm:^1.3.1":
version: 1.3.1
resolution: "which@npm:1.3.1"
@@ -10679,6 +10756,17 @@ __metadata:
languageName: node
linkType: hard
"wrap-ansi@npm:^6.2.0":
version: 6.2.0
resolution: "wrap-ansi@npm:6.2.0"
dependencies:
ansi-styles: ^4.0.0
string-width: ^4.1.0
strip-ansi: ^6.0.0
checksum: 6cd96a410161ff617b63581a08376f0cb9162375adeb7956e10c8cd397821f7eb2a6de24eb22a0b28401300bf228c86e50617cd568209b5f6775b93c97d2fe3a
languageName: node
linkType: hard
"wrap-ansi@npm:^7.0.0":
version: 7.0.0
resolution: "wrap-ansi@npm:7.0.0"
@@ -10775,6 +10863,13 @@ __metadata:
languageName: node
linkType: hard
"y18n@npm:^4.0.0":
version: 4.0.3
resolution: "y18n@npm:4.0.3"
checksum: 014dfcd9b5f4105c3bb397c1c8c6429a9df004aa560964fb36732bfb999bfe83d45ae40aeda5b55d21b1ee53d8291580a32a756a443e064317953f08025b1aa4
languageName: node
linkType: hard
"yallist@npm:^4.0.0":
version: 4.0.0
resolution: "yallist@npm:4.0.0"
@@ -10789,6 +10884,35 @@ __metadata:
languageName: node
linkType: hard
"yargs-parser@npm:^18.1.2":
version: 18.1.3
resolution: "yargs-parser@npm:18.1.3"
dependencies:
camelcase: ^5.0.0
decamelize: ^1.2.0
checksum: 60e8c7d1b85814594d3719300ecad4e6ae3796748b0926137bfec1f3042581b8646d67e83c6fc80a692ef08b8390f21ddcacb9464476c39bbdf52e34961dd4d9
languageName: node
linkType: hard
"yargs@npm:^15.3.1":
version: 15.4.1
resolution: "yargs@npm:15.4.1"
dependencies:
cliui: ^6.0.0
decamelize: ^1.2.0
find-up: ^4.1.0
get-caller-file: ^2.0.1
require-directory: ^2.1.1
require-main-filename: ^2.0.0
set-blocking: ^2.0.0
string-width: ^4.2.0
which-module: ^2.0.0
y18n: ^4.0.0
yargs-parser: ^18.1.2
checksum: 40b974f508d8aed28598087720e086ecd32a5fd3e945e95ea4457da04ee9bdb8bdd17fd91acff36dc5b7f0595a735929c514c40c402416bbb87c03f6fb782373
languageName: node
linkType: hard
"yocto-queue@npm:^0.1.0":
version: 0.1.0
resolution: "yocto-queue@npm:0.1.0"
+1 -1
View File
@@ -34,7 +34,7 @@
"techdocs-cli": "node scripts/techdocs-cli.js",
"techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js",
"prepare": "husky install",
"postinstall": "husky install"
"postinstall": "husky install || true"
},
"workspaces": {
"packages": [
@@ -31,8 +31,8 @@ parameters:
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
catalogFilter:
kind: Group
- title: Choose a location
required:
- repoUrl
+1
View File
@@ -84,6 +84,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
const graph = PackageGraph.fromPackages(packages);
const changedPackages = await graph.listChangedPackages({
ref: opts.since,
analyzeLockfile: true,
});
const withDevDependents = graph.collectPackageNames(
changedPackages.map(pkg => pkg.name),
+4 -1
View File
@@ -34,7 +34,10 @@ export async function command(opts: OptionValues): Promise<void> {
if (opts.since) {
const graph = PackageGraph.fromPackages(packages);
packages = await graph.listChangedPackages({ ref: opts.since });
packages = await graph.listChangedPackages({
ref: opts.since,
analyzeLockfile: true,
});
}
// Packages are ordered from most to least number of dependencies, as a
+1
View File
@@ -93,6 +93,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
const graph = PackageGraph.fromPackages(packages);
const changedPackages = await graph.listChangedPackages({
ref: opts.since,
analyzeLockfile: true,
});
const packageNames = Array.from(
@@ -21,7 +21,8 @@ import { serveBundle } from '../../lib/bundler';
import { loadCliConfig } from '../../lib/config';
import { paths } from '../../lib/paths';
import { Lockfile } from '../../lib/versioning';
import { includedFilter } from '../versions/lint';
import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint';
import { PackageGraph } from '../../lib/monorepo';
interface StartAppOptions {
verifyVersions?: boolean;
@@ -36,10 +37,13 @@ export async function startFrontend(options: StartAppOptions) {
const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
const result = lockfile.analyze({
filter: includedFilter,
localPackages: PackageGraph.fromPackages(
await PackageGraph.listTargetPackages(),
),
});
const problemPackages = [...result.newVersions, ...result.newRanges].map(
({ name }) => name,
);
const problemPackages = [...result.newVersions, ...result.newRanges]
.map(({ name }) => name)
.filter(forbiddenDuplicatesFilter);
if (problemPackages.length > 1) {
console.log(
+6 -1
View File
@@ -39,6 +39,7 @@ import {
ReleaseManifest,
} from '@backstage/release-manifests';
import 'global-agent/bootstrap';
import { PackageGraph } from '../../lib/monorepo';
const DEP_TYPES = [
'dependencies',
@@ -200,7 +201,7 @@ export default async (opts: OptionValues) => {
lockfile.remove(name, range);
}
}
await lockfile.save();
await lockfile.save(lockfilePath);
}
const breakingUpdates = new Map<string, { from: string; to: string }>();
@@ -305,8 +306,12 @@ export default async (opts: OptionValues) => {
// Finally we make sure the new lockfile doesn't have any duplicates
const dedupLockfile = await Lockfile.load(lockfilePath);
const result = dedupLockfile.analyze({
filter,
localPackages: PackageGraph.fromPackages(
await PackageGraph.listTargetPackages(),
),
});
if (result.newVersions.length > 0) {
+7 -2
View File
@@ -18,6 +18,7 @@ import { OptionValues } from 'commander';
import { Lockfile } from '../../lib/versioning';
import { paths } from '../../lib/paths';
import partition from 'lodash/partition';
import { PackageGraph } from '../../lib/monorepo';
// Packages that we try to avoid duplicates for
const INCLUDED = [/^@backstage\//];
@@ -52,9 +53,13 @@ export default async (cmd: OptionValues) => {
let success = true;
const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
const result = lockfile.analyze({
filter: includedFilter,
localPackages: PackageGraph.fromPackages(
await PackageGraph.listTargetPackages(),
),
});
logArray(
@@ -65,7 +70,7 @@ export default async (cmd: OptionValues) => {
if (fix) {
lockfile.replaceVersions(result.newVersions);
await lockfile.save();
await lockfile.save(lockfilePath);
} else {
const [newVersionsForbidden, newVersionsAllowed] = partition(
result.newVersions,
+20
View File
@@ -64,3 +64,23 @@ export async function listChangedFiles(ref: string) {
return Array.from(new Set([...tracked, ...untracked]));
}
/**
* Returns the contents of a file at a specific ref.
*/
export async function 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;
}
@@ -14,19 +14,25 @@
* limitations under the License.
*/
import { resolve as resolvePath } from 'path';
import { getPackages } from '@manypkg/get-packages';
import { PackageGraph } from './PackageGraph';
import { listChangedFiles } from '../git';
import { Lockfile } from '../versioning/Lockfile';
import { listChangedFiles, readFileAtRef } from '../git';
jest.mock('../git');
const mockListChangedFiles = listChangedFiles as jest.MockedFunction<
typeof listChangedFiles
>;
const mockReadFileAtRef = readFileAtRef as jest.MockedFunction<
typeof readFileAtRef
>;
jest.mock('../paths', () => ({
paths: {
targetRoot: '/',
resolveTargetRoot: (...paths: string[]) => resolvePath('/', ...paths),
},
}));
@@ -177,4 +183,54 @@ describe('PackageGraph', () => {
graph.listChangedPackages({ ref: 'origin/master' }),
).resolves.toEqual([graph.get('a'), graph.get('b')]);
});
it('lists changed packages with lockfile analysis', async () => {
const graph = PackageGraph.fromPackages(testPackages);
mockListChangedFiles.mockResolvedValueOnce(
['README.md', 'packages/a/src/foo.ts', 'yarn.lock'].sort(),
);
mockReadFileAtRef.mockResolvedValueOnce(`
a@^1:
version: "1.0.0"
c@^1:
version: "1.0.0"
dependencies:
c-dep: ^1
c-dep@^2:
version: "2.0.0"
integrity: sha512-xyz
`);
jest.spyOn(Lockfile, 'load').mockResolvedValueOnce(
Lockfile.parse(`
a@^1:
version: "1.0.0"
c@^1:
version: "1.0.0"
dependencies:
c-dep: ^1
c-dep@^2:
version: "2.0.0"
integrity: sha512-xyz-other
`),
);
await expect(
graph
.listChangedPackages({
ref: 'origin/master',
analyzeLockfile: true,
})
.then(pkgs => pkgs.map(pkg => pkg.name)),
).resolves.toEqual(['a', 'c']);
expect(mockReadFileAtRef).toHaveBeenCalledWith(
'yarn.lock',
'origin/master',
);
});
});
+77 -2
View File
@@ -18,7 +18,8 @@ import path from 'path';
import { getPackages, Package } from '@manypkg/get-packages';
import { paths } from '../paths';
import { PackageRole } from '../role';
import { listChangedFiles } from '../git';
import { listChangedFiles, readFileAtRef } from '../git';
import { Lockfile } from '../versioning';
type PackageJSON = Package['packageJson'];
@@ -191,7 +192,10 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
return targets;
}
async listChangedPackages(options: { ref: string }) {
async listChangedPackages(options: {
ref: string;
analyzeLockfile?: boolean;
}) {
const changedFiles = await listChangedFiles(options.ref);
const dirMap = new Map(
@@ -234,6 +238,77 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
}
}
if (changedFiles.includes('yarn.lock') && options.analyzeLockfile) {
// Load the lockfile in the working tree and the one at the ref and diff them
let thisLockfile: Lockfile;
let otherLockfile: Lockfile;
try {
thisLockfile = await Lockfile.load(
paths.resolveTargetRoot('yarn.lock'),
);
otherLockfile = Lockfile.parse(
await readFileAtRef('yarn.lock', options.ref),
);
} catch (error) {
console.warn(
`Failed to read lockfiles, assuming all packages have changed, ${error}`,
);
return Array.from(this.values());
}
const diff = thisLockfile.diff(otherLockfile);
// Create a simplified dependency graph only keeps track of package names
const graph = thisLockfile.createSimplifiedDependencyGraph();
// Merge the dependency graph from the other lockfile into this one in
// order to be able to detect removals accurately.
{
const otherGraph = thisLockfile.createSimplifiedDependencyGraph();
for (const [name, dependencies] of otherGraph) {
const node = graph.get(name);
if (node) {
dependencies.forEach(d => node.add(d));
} else {
graph.set(name, dependencies);
}
}
}
// The check is simplified by only considering the package names rather
// than the exact version range queries that were changed.
// TODO(Rugvip): Use a more exact check
const changedPackages = new Set(
[...diff.added, ...diff.changed, ...diff.removed].map(e => e.name),
);
// Starting with our set of changed packages from the diff, we loop through
// the full graph and add any package that has a dependency on a changed package.
// We keep looping until all transitive dependencies have been detected.
let changed = false;
do {
changed = false;
for (const [name, dependencies] of graph) {
if (changedPackages.has(name)) {
continue;
}
for (const dep of dependencies) {
if (changedPackages.has(dep)) {
changed = true;
changedPackages.add(name);
break;
}
}
}
} while (changed);
// Add all local packages that had a transitive dependency change to the result set
for (const node of this.values()) {
if (changedPackages.has(node.name) && !result.includes(node)) {
result.push(node);
}
}
}
return result;
}
}
@@ -16,7 +16,7 @@
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { resolve as resolvePath } from 'path';
import { resolve as resolvePath, join as joinPath } from 'path';
import { paths } from '../../paths';
import { Task } from '../../tasks';
import { FactoryRegistry } from '../FactoryRegistry';
@@ -73,7 +73,7 @@ describe('webLibraryPackage factory', () => {
'',
`Creating web-library package ${expectedwebLibraryPackageName}`,
'Checking Prerequisites:',
`availability packages/${expectedwebLibraryPackageName}`,
`availability ${joinPath('packages', expectedwebLibraryPackageName)}`,
'creating temp dir',
'Executing Template:',
'copying .eslintrc.js',
@@ -82,7 +82,7 @@ describe('webLibraryPackage factory', () => {
'templating index.ts.hbs',
'copying setupTests.ts',
'Installing:',
`moving packages/${expectedwebLibraryPackageName}`,
`moving ${joinPath('packages', expectedwebLibraryPackageName)}`,
]);
await expect(
+568 -27
View File
@@ -16,14 +16,23 @@
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { ExtendedPackage } from '../monorepo';
import { Lockfile } from './Lockfile';
const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
`;
const mockA = `${HEADER}
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
`;
const mockA = `${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
@@ -38,7 +47,7 @@ b@^2:
version "2.0.0"
`;
const mockADedup = `${HEADER}
const mockADedup = `${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
@@ -50,7 +59,7 @@ b@2.0.x, b@^2:
version "2.0.1"
`;
const mockB = `${HEADER}
const mockB = `${LEGACY_HEADER}
"@s/a@*", "@s/a@1 || 2", "@s/a@^1":
version "1.0.1"
@@ -58,7 +67,7 @@ const mockB = `${HEADER}
version "2.0.0"
`;
const mockBDedup = `${HEADER}
const mockBDedup = `${LEGACY_HEADER}
"@s/a@*", "@s/a@1 || 2", "@s/a@^2.0.x":
version "2.0.0"
@@ -77,10 +86,12 @@ describe('Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]);
expect(lockfile.get('a')).toEqual([
{ range: '^1', version: '1.0.1', dataKey: 'a@^1' },
]);
expect(lockfile.get('b')).toEqual([
{ range: '2.0.x', version: '2.0.1' },
{ range: '^2', version: '2.0.0' },
{ range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' },
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
]);
expect(lockfile.toString()).toBe(mockA);
});
@@ -91,7 +102,7 @@ describe('Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze();
const result = lockfile.analyze({ localPackages: new Map() });
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
@@ -110,7 +121,7 @@ describe('Lockfile', () => {
expect(lockfile.toString()).toBe(mockADedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockA);
await expect(lockfile.save()).resolves.toBeUndefined();
await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockADedup);
});
@@ -120,7 +131,7 @@ describe('Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze();
const result = lockfile.analyze({ localPackages: new Map() });
expect(result).toEqual({
invalidRanges: [],
newRanges: [
@@ -154,15 +165,7 @@ describe('Lockfile', () => {
});
});
const newHeader = `# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 6
cacheKey: 8
`;
const mockANew = `${newHeader}
const mockANew = `${MODERN_HEADER}
a@^1:
version: 1.0.1
dependencies:
@@ -177,7 +180,7 @@ b@^2:
version: 2.0.0
`;
const mockANewDedup = `${newHeader}
const mockANewDedup = `${MODERN_HEADER}
a@^1:
version: 1.0.1
dependencies:
@@ -192,6 +195,36 @@ b@^2:
version: 2.0.1
`;
const mockANewLocal = `${MODERN_HEADER}
a@^1:
version: 1.0.1
dependencies:
b: ^2
integrity: sha512-xyz
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
"b@2.0.x, b@^2.0.1":
version: 0.0.0-use.local
b@^2:
version: 2.0.0
`;
const mockANewLocalDedup = `${MODERN_HEADER}
a@^1:
version: 1.0.1
dependencies:
b: ^2
integrity: sha512-xyz
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
"b@2.0.x, b@^2.0.1":
version: 0.0.0-use.local
b@^2:
version: 0.0.0-use.local
`;
describe('New Lockfile', () => {
afterEach(() => {
mockFs.restore();
@@ -203,11 +236,13 @@ describe('New Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]);
expect(lockfile.get('a')).toEqual([
{ range: '^1', version: '1.0.1', dataKey: 'a@^1' },
]);
expect(lockfile.get('b')).toEqual([
{ range: '2.0.x', version: '2.0.1' },
{ range: '^2.0.1', version: '2.0.1' },
{ range: '^2', version: '2.0.0' },
{ range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' },
{ range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' },
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
]);
expect(lockfile.toString()).toBe(mockANew);
});
@@ -218,7 +253,7 @@ describe('New Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze();
const result = lockfile.analyze({ localPackages: new Map() });
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
@@ -237,9 +272,515 @@ describe('New Lockfile', () => {
expect(lockfile.toString()).toBe(mockANewDedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockANew);
await expect(lockfile.save()).resolves.toBeUndefined();
await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewDedup,
);
});
it('should deduplicate and save mockANewLocal', async () => {
mockFs({
'/yarn.lock': mockANewLocal,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze({
localPackages: new Map([
[
'b',
{
packageJson: { version: '2.0.1' },
} as ExtendedPackage,
],
]),
});
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
newVersions: [
{
name: 'b',
range: '^2',
oldVersion: '2.0.0',
newVersion: '0.0.0-use.local',
},
],
});
expect(lockfile.toString()).toBe(mockANewLocal);
lockfile.replaceVersions(result.newVersions);
expect(lockfile.toString()).toBe(mockANewLocalDedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewLocal,
);
await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewLocalDedup,
);
});
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()],
]),
);
});
});
});
+146 -41
View File
@@ -18,6 +18,7 @@ import fs from 'fs-extra';
import semver from 'semver';
import { parseSyml, stringifySyml } from '@yarnpkg/parsers';
import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile';
import { ExtendedPackage } from '../monorepo';
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
@@ -25,14 +26,28 @@ type LockfileData = {
[entry: string]: {
version: string;
resolved?: string;
integrity?: string;
integrity?: string /* old */;
checksum?: string /* new */;
dependencies?: { [name: string]: string };
peerDependencies?: { [name: string]: string };
};
};
type LockfileQueryEntry = {
range: string;
version: string;
dataKey: string;
};
type LockfileDiffEntry = {
name: string;
range: string;
};
type LockfileDiff = {
added: LockfileDiffEntry[];
changed: LockfileDiffEntry[];
removed: LockfileDiffEntry[];
};
/** Entries that have an invalid version range, for example an npm tag */
@@ -64,20 +79,6 @@ type AnalyzeResult = {
newRanges: AnalyzeResultNewRange[];
};
function parseLockfile(lockfileContents: string) {
try {
return {
object: parseSyml(lockfileContents),
type: 'success',
};
} catch (err) {
return {
object: null,
type: err,
};
}
}
// the new yarn header is handled out of band of the parsing
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746
const NEW_HEADER = `${[
@@ -85,11 +86,6 @@ const NEW_HEADER = `${[
`# Manual changes might be lost - proceed with caution!\n`,
].join(``)}\n`;
function stringifyLockfile(data: LockfileData, legacy: boolean) {
return legacy
? legacyStringifyLockfile(data)
: NEW_HEADER + stringifySyml(data);
}
// taken from yarn parser package
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136
const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i;
@@ -110,13 +106,19 @@ const SPECIAL_OBJECT_KEYS = [
export class Lockfile {
static async load(path: string) {
const lockfileContents = await fs.readFile(path, 'utf8');
const legacy = LEGACY_REGEX.test(lockfileContents);
const lockfile = parseLockfile(lockfileContents);
if (lockfile.type !== 'success') {
throw new Error(`Failed yarn.lock parse with ${lockfile.type}`);
return Lockfile.parse(lockfileContents);
}
static parse(content: string) {
const legacy = LEGACY_REGEX.test(content);
let data: LockfileData;
try {
data = parseSyml(content);
} catch (err) {
throw new Error(`Failed yarn.lock parse, ${err}`);
}
const data = lockfile.object as LockfileData;
const packages = new Map<string, LockfileQueryEntry[]>();
for (const [key, value] of Object.entries(data)) {
@@ -139,15 +141,14 @@ export class Lockfile {
if (range.startsWith('npm:')) {
range = range.slice('npm:'.length);
}
queries.push({ range, version: value.version });
queries.push({ range, version: value.version, dataKey: key });
}
}
return new Lockfile(path, packages, data, legacy);
return new Lockfile(packages, data, legacy);
}
private constructor(
private readonly path: string,
private readonly packages: Map<string, LockfileQueryEntry[]>,
private readonly data: LockfileData,
private readonly legacy: boolean = false,
@@ -164,8 +165,11 @@ export class Lockfile {
}
/** Analyzes the lockfile to identify possible actions and warnings for the entries */
analyze(options?: { filter?: (name: string) => boolean }): AnalyzeResult {
const { filter } = options ?? {};
analyze(options: {
filter?: (name: string) => boolean;
localPackages: Map<string, ExtendedPackage>;
}): AnalyzeResult {
const { filter, localPackages } = options;
const result: AnalyzeResult = {
invalidRanges: [],
newVersions: [],
@@ -178,7 +182,9 @@ export class Lockfile {
}
// Get rid of and signal any invalid ranges upfront
const invalid = allEntries.filter(e => !semver.validRange(e.range));
const invalid = allEntries.filter(
e => !semver.validRange(e.range) && !e.range.startsWith('workspace:'),
);
result.invalidRanges.push(
...invalid.map(({ range }) => ({ name, range })),
);
@@ -190,36 +196,55 @@ export class Lockfile {
}
// Find all versions currently in use
const versions = Array.from(new Set(entries.map(e => e.version))).sort(
(v1, v2) => semver.rcompare(v1, v2),
);
const versions = Array.from(new Set(entries.map(e => e.version)))
.map(v => {
// Translate workspace:^ references to the actual version
if (v === '0.0.0-use.local') {
const local = localPackages.get(name);
if (!local) {
throw new Error(`No local package found for ${name}`);
}
if (!local.packageJson.version) {
throw new Error(`No version found for local package ${name}`);
}
return {
entryVersion: v,
actualVersion: local.packageJson.version,
};
}
return { entryVersion: v, actualVersion: v };
})
.sort((v1, v2) => semver.rcompare(v1.actualVersion, v2.actualVersion));
// If we're not using at least 2 different versions we're done
if (versions.length < 2) {
continue;
}
// TODO(Rugvip): Support bumping into workspace ranges too
const acceptedVersions = new Set<string>();
for (const { version, range } of entries) {
// Finds the highest matching version from the the known versions
// TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one
const acceptedVersion = versions.find(v => semver.satisfies(v, range));
const acceptedVersion = versions.find(v =>
semver.satisfies(v.actualVersion, range),
);
if (!acceptedVersion) {
throw new Error(
`No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`,
);
}
if (acceptedVersion !== version) {
if (acceptedVersion.entryVersion !== version) {
result.newVersions.push({
name,
range,
newVersion: acceptedVersion,
newVersion: acceptedVersion.entryVersion,
oldVersion: version,
});
}
acceptedVersions.add(acceptedVersion);
acceptedVersions.add(acceptedVersion.actualVersion);
}
// If all ranges were able to accept the same version, we're done
@@ -315,11 +340,91 @@ export class Lockfile {
}
}
async save() {
await fs.writeFile(this.path, this.toString(), 'utf8');
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;
}
async save(path: string) {
await fs.writeFile(path, this.toString(), 'utf8');
}
toString() {
return stringifyLockfile(this.data, this.legacy);
return this.legacy
? legacyStringifyLockfile(this.data)
: NEW_HEADER + stringifySyml(this.data);
}
}
+3 -3
View File
@@ -35,9 +35,6 @@
"@manypkg/get-packages": "^1.1.3",
"@microsoft/api-documenter": "^7.17.11",
"@microsoft/api-extractor": "^7.23.0",
"@microsoft/api-extractor-model": "^7.17.2",
"@microsoft/tsdoc": "0.14.1",
"@microsoft/tsdoc-config": "0.16.2",
"chalk": "^4.0.0",
"commander": "^9.1.0",
"fs-extra": "10.1.0",
@@ -53,6 +50,9 @@
"mock-fs": "^5.1.0"
},
"peerDependencies": {
"@microsoft/api-extractor-model": "*",
"@microsoft/tsdoc": "*",
"@microsoft/tsdoc-config": "*",
"@rushstack/node-core-library": "*",
"prettier": "^2.8.1",
"typescript": "> 3.0.0"
+18 -15
View File
@@ -15,7 +15,7 @@
*/
import mockFs from 'mock-fs';
import { resolve as resolvePath } from 'path';
import { resolve as resolvePath, join as joinPath } from 'path';
import { resolvePackagePath, paths, findPackageDirs } from './paths';
describe('paths', () => {
@@ -61,15 +61,15 @@ describe('paths', () => {
});
it('should return the path to the package if it exists and has a package.json', async () => {
expect(await resolvePackagePath('packages/package-a')).toBe(
'packages/package-a',
joinPath('packages', 'package-a'),
);
expect(await resolvePackagePath('packages/package-b')).toBe(
'packages/package-b',
joinPath('packages', 'package-b'),
);
});
it('should work with absolute paths', async () => {
expect(await resolvePackagePath('/root/packages/package-a')).toBe(
'packages/package-a',
joinPath('packages', 'package-a'),
);
});
it('should return undefined if the pat is not a directory', async () => {
@@ -79,19 +79,19 @@ describe('paths', () => {
describe('findPackageDirs', () => {
it('should return only the given packages', async () => {
expect(await findPackageDirs(['packages/package-a'])).toEqual([
'packages/package-a',
joinPath('packages', 'package-a'),
]);
});
it('should return only the given packages when using glob patterns', async () => {
expect(await findPackageDirs(['packages/*'])).toEqual([
'packages/package-a',
'packages/package-b',
joinPath('packages', 'package-a'),
joinPath('packages', 'package-b'),
]);
expect(await findPackageDirs(['packages/*', 'plugins/*'])).toEqual([
'packages/package-a',
'packages/package-b',
'plugins/plugin-a',
'plugins/plugin-b',
joinPath('packages', 'package-a'),
joinPath('packages', 'package-b'),
joinPath('plugins', 'plugin-a'),
joinPath('plugins', 'plugin-b'),
]);
});
it('should return only the given packages when using absolute paths', async () => {
@@ -100,15 +100,18 @@ describe('paths', () => {
'/root/packages/package-a',
'/root/plugins/plugin-b',
]),
).toEqual(['packages/package-a', 'plugins/plugin-b']);
).toEqual([
joinPath('packages', 'package-a'),
joinPath('plugins', 'plugin-b'),
]);
});
it('should return only the given packages when using absolute paths with glob patterns', async () => {
expect(
await findPackageDirs(['/root/packages/*', '/root/plugins/*-a']),
).toEqual([
'packages/package-a',
'packages/package-b',
'plugins/plugin-a',
joinPath('packages', 'package-a'),
joinPath('packages', 'package-b'),
joinPath('plugins', 'plugin-a'),
]);
});
});
+1
View File
@@ -11,6 +11,7 @@
### Patch Changes
- 2cb9998: Fixed bug in `EntityOwnerPicker` and `EntityLifecyclePicker` that filtered on unavailable tags for the selected kind.
- 2e701b3796: Internal refactor to use `react-router-dom` rather than `react-router`.
- 6ffa47bb0a: Cleanup and small fixes for the kind selector
- 19356df560: Updated dependency `zen-observable` to `^0.9.0`.
@@ -202,4 +202,33 @@ describe('<EntityTagPicker/>', () => {
tags: new EntityTagFilter(['tag2']),
});
});
it('removes tags from filters if there are none available', async () => {
const updateFilters = jest.fn();
const mockCatalogApiRefNoTags = {
getEntityFacets: async () => ({
facets: {
'metadata.tags': {},
},
}),
} as unknown as CatalogApi;
render(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRefNoTags]]}>
<MockEntityListContextProvider
value={{
updateFilters,
queryParameters: { tags: ['tag1'] },
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>
,
</TestApiProvider>,
);
await waitFor(() =>
expect(updateFilters).toHaveBeenLastCalledWith({
tags: undefined,
}),
);
});
});
@@ -93,10 +93,14 @@ export const EntityTagPicker = (props: EntityTagPickerProps) => {
}, [queryParamTags]);
useEffect(() => {
const tags = Object.keys(availableTags ?? {});
updateFilters({
tags: selectedTags.length ? new EntityTagFilter(selectedTags) : undefined,
tags:
selectedTags.length && tags.length
? new EntityTagFilter(selectedTags)
: undefined,
});
}, [selectedTags, updateFilters]);
}, [selectedTags, updateFilters, availableTags]);
if (!Object.keys(availableTags ?? {}).length) return null;
@@ -3,6 +3,7 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import express from 'express';
import { Logger } from 'winston';
@@ -15,6 +16,8 @@ export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public
export interface RouterOptions {
// (undocumented)
catalogApi?: CatalogApi;
// (undocumented)
config: Config;
// (undocumented)
@@ -209,7 +209,7 @@ describe('CodeCoverageUtils', () => {
} catch (error: any) {
err = error;
}
expect(err?.message).toEqual('Content-Type missing');
expect(err?.message).toEqual('Content-Type header missing');
});
it('rejects unsupported content type', () => {
@@ -223,7 +223,9 @@ describe('CodeCoverageUtils', () => {
} catch (error: any) {
err = error;
}
expect(err?.message).toEqual('Illegal Content-Type');
expect(err?.message).toEqual(
'Content-Type header "application/json" not supported, expected "text/xml" possibly followed by a charset',
);
});
it('parses the body', () => {
@@ -162,9 +162,11 @@ export class CoverageUtils {
validateRequestBody(req: Request) {
const contentType = req.headers['content-type'];
if (!contentType) {
throw new InputError('Content-Type missing');
throw new InputError('Content-Type header missing');
} else if (!contentType.match(/^text\/xml($|;)/)) {
throw new InputError('Illegal Content-Type');
throw new InputError(
`Content-Type header "${contentType}" not supported, expected "text/xml" possibly followed by a charset`,
);
}
const body = req.body;
if (!body) {
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BranchHit, FileEntry } from '../types';
import { JacocoSourceFile, JacocoXML } from './types';
import { Logger } from 'winston';
@@ -46,6 +46,7 @@ export interface RouterOptions {
database: PluginDatabaseManager;
urlReader: UrlReader;
logger: Logger;
catalogApi?: CatalogApi;
}
export interface CodeCoverageApi {
@@ -59,7 +60,8 @@ export const makeRouter = async (
const codeCoverageDatabase = await CodeCoverageDatabase.create(database);
const codecovUrl = await discovery.getExternalBaseUrl('code-coverage');
const catalogApi: CatalogApi = new CatalogClient({ discoveryApi: discovery });
const catalogApi =
options.catalogApi ?? new CatalogClient({ discoveryApi: discovery });
const scm = ScmIntegrations.fromConfig(config);
const router = Router();
@@ -167,10 +169,10 @@ export const makeRouter = async (
* /report?entity=component:default/mycomponent&coverageType=cobertura
*/
router.post('/report', async (req, res) => {
const { entity, coverageType } = req.query;
const entityLookup = await catalogApi.getEntityByRef(entity as string);
if (!entityLookup) {
throw new NotFoundError(`No entity found matching ${entity}`);
const { entity: entityRef, coverageType } = req.query;
const entity = await catalogApi.getEntityByRef(entityRef as string);
if (!entity) {
throw new NotFoundError(`No entity found matching ${entityRef}`);
}
let converter: Converter;
@@ -185,7 +187,7 @@ export const makeRouter = async (
}
const { sourceLocation, vcs, scmFiles, body } =
await utils.processCoveragePayload(entityLookup, req);
await utils.processCoveragePayload(entity, req);
const files = converter.convert(body, scmFiles);
if (!files || files.length === 0) {
@@ -193,7 +195,7 @@ export const makeRouter = async (
}
const coverage = await utils.buildCoverage(
entityLookup,
entity,
sourceLocation,
vcs,
files,
@@ -204,7 +206,7 @@ export const makeRouter = async (
links: [
{
rel: 'coverage',
href: `${codecovUrl}/report?entity=${entity}`,
href: `${codecovUrl}/report?entity=${entityRef}`,
},
],
});
@@ -21,6 +21,8 @@ import {
UrlReaders,
useHotMemoize,
} from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
import { CompoundEntityRef, parseEntityRef } from '@backstage/catalog-model';
import { Server } from 'http';
import knexFactory from 'knex';
import { Logger } from 'winston';
@@ -52,13 +54,27 @@ export async function startStandaloneServer(
return knex;
});
const catalogApi = {
async getEntityByRef(entityRef: string | CompoundEntityRef) {
const { kind, namespace, name } = parseEntityRef(entityRef);
return {
apiVersion: 'backstage.io/v1alpha1',
kind,
metadata: { name, namespace },
spec: {},
};
},
} as Partial<CatalogApi> as CatalogApi;
logger.debug('Starting application server...');
const router = await createRouter({
database: { getClient: async () => db },
config,
discovery: SingleHostDiscovery.fromConfig(config),
urlReader: UrlReaders.default({ logger, config }),
logger,
catalogApi,
});
let service = createServiceBuilder(module)
@@ -44,8 +44,10 @@ import useAsync from 'react-use/lib/useAsync';
const useStyles = makeStyles((theme: BackstageTheme) => ({
graph: {
minHeight: '100%',
flex: 1,
},
graphWrapper: {
display: 'flex',
height: '100%',
},
organizationNode: {
@@ -86,7 +86,7 @@ export const ServicesTable = ({
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => (
<Typography>{rowData.uptime.uptimePercentage.p90}</Typography>
<Typography>{rowData.uptime?.uptimePercentage?.p90 || ''}</Typography>
),
};
const actionsColumn: TableColumn<Service> = {
@@ -73,16 +73,16 @@ spec:
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
catalogFilter:
kind: Group
system:
title: System
type: string
description: System of the component
ui:field: EntityPicker
ui:options:
allowedKinds:
- System
catalogFilter:
kind: System
defaultKind: System
- title: Choose a location
@@ -74,16 +74,16 @@ spec:
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
catalogFilter:
kind: Group
system:
title: System
type: string
description: System of the component
ui:field: EntityPicker
ui:options:
allowedKinds:
- System
catalogFilter:
kind: System
defaultKind: System
- title: Choose a location
@@ -73,16 +73,16 @@ spec:
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
catalogFilter:
kind: Group
system:
title: System
type: string
description: System of the component
ui:field: EntityPicker
ui:options:
allowedKinds:
- System
catalogFilter:
kind: System
defaultKind: System
- title: Choose a location
@@ -49,8 +49,8 @@ spec:
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
catalogFilter:
kind: Group
steps:
- id: fetch-base
+16
View File
@@ -105,6 +105,10 @@ export const EntityPickerFieldExtension: FieldExtensionComponent<
defaultNamespace?: string | false | undefined;
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
catalogFilter?:
| Record<string, string | string[]>
| Record<string, string | string[]>[]
| undefined;
}
>;
@@ -116,6 +120,10 @@ export const EntityPickerFieldSchema: FieldSchema<
defaultNamespace?: string | false | undefined;
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
catalogFilter?:
| Record<string, string | string[]>
| Record<string, string | string[]>[]
| undefined;
}
>;
@@ -326,6 +334,10 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent<
defaultNamespace?: string | false | undefined;
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
catalogFilter?:
| Record<string, string | string[]>
| Record<string, string | string[]>[]
| undefined;
}
>;
@@ -336,6 +348,10 @@ export const OwnerPickerFieldSchema: FieldSchema<
defaultNamespace?: string | false | undefined;
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
catalogFilter?:
| Record<string, string | string[]>
| Record<string, string | string[]>[]
| undefined;
}
>;
@@ -53,8 +53,8 @@ parameters:
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
catalogFilter:
kind: Group
- title: Choose a location
required:
- repoUrl
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { EntityFilterQuery } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
@@ -34,7 +35,13 @@ describe('<EntityPicker />', () => {
const schema = {};
const required = false;
let uiSchema: {
'ui:options': { allowedKinds?: string[]; defaultKind?: string };
'ui:options': {
allowedKinds?: string[];
defaultKind?: string;
allowArbitraryValues?: boolean;
defaultNamespace?: string | false;
catalogFilter?: EntityFilterQuery;
};
};
const rawErrors: string[] = [];
const formData = undefined;
@@ -66,7 +73,7 @@ describe('<EntityPicker />', () => {
afterEach(() => jest.resetAllMocks());
describe('without allowedKinds', () => {
describe('without allowedKinds and catalogFilter', () => {
beforeEach(() => {
uiSchema = { 'ui:options': {} };
props = {
@@ -136,4 +143,97 @@ describe('<EntityPicker />', () => {
});
});
});
describe('with catalogFilter', () => {
beforeEach(() => {
uiSchema = {
'ui:options': {
catalogFilter: [
{
kind: ['Group'],
'metadata.name': 'test-entity',
},
{
kind: ['User'],
'metadata.name': 'test-entity',
},
],
},
};
props = {
onChange,
schema,
required,
uiSchema,
rawErrors,
formData,
} as unknown as FieldProps<any>;
catalogApi.getEntities.mockResolvedValue({ items: entities });
});
it('searches for a specific group entity', async () => {
await renderInTestApp(
<Wrapper>
<EntityPicker {...props} />
</Wrapper>,
);
expect(catalogApi.getEntities).toHaveBeenCalledWith({
filter: [
{
kind: ['Group'],
'metadata.name': 'test-entity',
},
{
kind: ['User'],
'metadata.name': 'test-entity',
},
],
});
});
});
describe('catalogFilter should take precedence over allowedKinds', () => {
beforeEach(() => {
uiSchema = {
'ui:options': {
catalogFilter: [
{
kind: ['Group'],
'metadata.name': 'test-group',
},
],
allowedKinds: ['User'],
},
};
props = {
onChange,
schema,
required,
uiSchema,
rawErrors,
formData,
} as unknown as FieldProps<any>;
catalogApi.getEntities.mockResolvedValue({ items: entities });
});
it('searches for a Group entity', async () => {
await renderInTestApp(
<Wrapper>
<EntityPicker {...props} />
</Wrapper>,
);
expect(catalogApi.getEntities).toHaveBeenCalledWith({
filter: [
{
kind: ['Group'],
'metadata.name': 'test-group',
},
],
});
});
});
});
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type EntityFilterQuery } from '@backstage/catalog-client';
import { useApi } from '@backstage/core-plugin-api';
import {
catalogApiRef,
@@ -44,6 +45,11 @@ export const EntityPicker = (props: EntityPickerProps) => {
idSchema,
} = props;
const allowedKinds = uiSchema['ui:options']?.allowedKinds;
const catalogFilter: EntityFilterQuery | undefined =
uiSchema['ui:options']?.catalogFilter ||
(allowedKinds && { kind: allowedKinds });
const defaultKind = uiSchema['ui:options']?.defaultKind;
const defaultNamespace = uiSchema['ui:options']?.defaultNamespace;
@@ -51,7 +57,7 @@ export const EntityPicker = (props: EntityPickerProps) => {
const { value: entities, loading } = useAsync(() =>
catalogApi.getEntities(
allowedKinds ? { filter: { kind: allowedKinds } } : undefined,
catalogFilter ? { filter: catalogFilter } : undefined,
),
);
@@ -16,16 +16,28 @@
import { z } from 'zod';
import { makeFieldSchemaFromZod } from '../utils';
/**
* @public
*/
export const entityQueryFilterExpressionSchema = z.record(
z.string().or(z.array(z.string())),
);
/**
* @public
*/
export const EntityPickerFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
/**
* @deprecated Use `catalogFilter` instead.
*/
allowedKinds: z
.array(z.string())
.optional()
.describe('List of kinds of entities to derive options from'),
.describe(
'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from',
),
defaultKind: z
.string()
.optional()
@@ -42,6 +54,11 @@ export const EntityPickerFieldSchema = makeFieldSchemaFromZod(
.describe(
'The default namespace. Options with this namespace will not be prefixed.',
),
catalogFilter: z
.array(entityQueryFilterExpressionSchema)
.or(entityQueryFilterExpressionSchema)
.optional()
.describe('List of key-value filter expression for entities'),
}),
);
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { type EntityFilterQuery } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
@@ -32,7 +33,15 @@ describe('<OwnerPicker />', () => {
const onChange = jest.fn();
const schema = {};
const required = false;
let uiSchema: { 'ui:options': { allowedKinds?: string[] } };
let uiSchema: {
'ui:options': {
allowedKinds?: string[];
defaultKind?: string;
allowArbitraryValues?: boolean;
defaultNamespace?: string | false;
catalogFilter?: EntityFilterQuery;
};
};
const rawErrors: string[] = [];
const formData = undefined;
@@ -63,7 +72,7 @@ describe('<OwnerPicker />', () => {
afterEach(() => jest.resetAllMocks());
describe('without allowedKinds', () => {
describe('without catalogFilter and allowedKinds', () => {
beforeEach(() => {
uiSchema = { 'ui:options': {} };
props = {
@@ -108,7 +117,7 @@ describe('<OwnerPicker />', () => {
catalogApi.getEntities.mockResolvedValue({ items: entities });
});
it('searches for users and groups', async () => {
it('searches for users', async () => {
await renderInTestApp(
<Wrapper>
<OwnerPicker {...props} />
@@ -122,4 +131,93 @@ describe('<OwnerPicker />', () => {
});
});
});
describe('with catalogFilter', () => {
beforeEach(() => {
uiSchema = {
'ui:options': {
catalogFilter: [
{
kind: ['Group'],
'spec.type': 'team',
},
],
},
};
props = {
onChange,
schema,
required,
uiSchema,
rawErrors,
formData,
} as unknown as FieldProps<any>;
catalogApi.getEntities.mockResolvedValue({ items: entities });
});
it('searches for group entities of type team', async () => {
await renderInTestApp(
<Wrapper>
<OwnerPicker {...props} />
</Wrapper>,
);
expect(catalogApi.getEntities).toHaveBeenCalledWith({
filter: [
{
kind: ['Group'],
'spec.type': 'team',
},
],
});
});
});
describe('catalogFilter should take precedence over allowedKinds', () => {
beforeEach(() => {
uiSchema = {
'ui:options': {
allowedKinds: ['User'],
catalogFilter: [
{
kind: ['Group', 'User'],
},
{
'spec.type': ['team', 'business-unit'],
},
],
},
};
props = {
onChange,
schema,
required,
uiSchema,
rawErrors,
formData,
} as unknown as FieldProps<any>;
catalogApi.getEntities.mockResolvedValue({ items: entities });
});
it('searches for users and groups or teams and business units', async () => {
await renderInTestApp(
<Wrapper>
<OwnerPicker {...props} />
</Wrapper>,
);
expect(catalogApi.getEntities).toHaveBeenCalledWith({
filter: [
{
kind: ['Group', 'User'],
},
{
'spec.type': ['team', 'business-unit'],
},
],
});
});
});
});
@@ -33,14 +33,16 @@ export const OwnerPicker = (props: OwnerPickerProps) => {
} = props;
const defaultNamespace = uiSchema['ui:options']?.defaultNamespace;
const allowedKinds = uiSchema['ui:options']?.allowedKinds;
const catalogFilter = uiSchema['ui:options']?.catalogFilter || {
kind: allowedKinds || ['Group', 'User'],
};
const ownerUiSchema = {
...uiSchema,
'ui:options': {
allowedKinds: (uiSchema['ui:options']?.allowedKinds || [
'Group',
'User',
]) as string[],
catalogFilter,
defaultKind: 'Group',
allowArbitraryValues:
uiSchema['ui:options']?.allowArbitraryValues ?? true,
@@ -15,6 +15,7 @@
*/
import { z } from 'zod';
import { makeFieldSchemaFromZod } from '../utils';
import { entityQueryFilterExpressionSchema } from '../EntityPicker/schema';
/**
* @public
@@ -22,12 +23,15 @@ import { makeFieldSchemaFromZod } from '../utils';
export const OwnerPickerFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
/**
* @deprecated Use `catalogFilter` instead.
*/
allowedKinds: z
.array(z.string())
.default(['Group', 'User'])
.optional()
.describe(
'List of kinds of entities to derive options from. Defaults to Group and User',
'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from. Defaults to Group and User',
),
allowArbitraryValues: z
.boolean()
@@ -39,6 +43,11 @@ export const OwnerPickerFieldSchema = makeFieldSchemaFromZod(
.describe(
'The default namespace. Options with this namespace will not be prefixed.',
),
catalogFilter: z
.array(entityQueryFilterExpressionSchema)
.or(entityQueryFilterExpressionSchema)
.optional()
.describe('List of key-value filter expression for entities'),
}),
);
@@ -20,6 +20,7 @@ import { renderInTestApp } from '@backstage/test-utils';
import { act, fireEvent } from '@testing-library/react';
import type { RJSFValidationError } from '@rjsf/utils';
import { JsonValue } from '@backstage/types';
import { NextFieldExtensionComponentProps } from '../../../extensions/types';
describe('Stepper', () => {
it('should render the step titles for each step of the manifest', async () => {
@@ -110,6 +111,98 @@ describe('Stepper', () => {
);
});
it('should merge nested formData correctly in multiple steps', async () => {
const Repo = ({
onChange,
}: NextFieldExtensionComponentProps<{ repository: string }, any>) => (
<input
aria-label="repo"
type="text"
onChange={e => onChange({ repository: e.target.value })}
defaultValue=""
/>
);
const Owner = ({
onChange,
}: NextFieldExtensionComponentProps<{ owner: string }, any>) => (
<input
aria-label="owner"
type="text"
onChange={e => onChange({ owner: e.target.value })}
defaultValue=""
/>
);
const manifest: TemplateParameterSchema = {
steps: [
{
title: 'Step 1',
schema: {
properties: {
first: {
type: 'object',
'ui:field': 'Repo',
},
},
},
},
{
title: 'Step 2',
schema: {
properties: {
second: {
type: 'object',
'ui:field': 'Owner',
},
},
},
},
],
title: 'React JSON Schema Form Test',
};
const onComplete = jest.fn(async (values: Record<string, JsonValue>) => {
expect(values).toEqual({
first: { repository: 'Repo' },
second: { owner: 'Owner' },
});
});
const { getByRole } = await renderInTestApp(
<Stepper
manifest={manifest}
onComplete={onComplete}
extensions={[
{ name: 'Repo', component: Repo },
{ name: 'Owner', component: Owner },
]}
/>,
);
await fireEvent.change(getByRole('textbox', { name: 'repo' }), {
target: { value: 'Repo' },
});
await act(async () => {
await fireEvent.click(getByRole('button', { name: 'Next' }));
});
await fireEvent.change(getByRole('textbox', { name: 'owner' }), {
target: { value: 'Owner' },
});
await act(async () => {
await fireEvent.click(getByRole('button', { name: 'Review' }));
});
await act(async () => {
await fireEvent.click(getByRole('button', { name: 'Create' }));
});
expect(onComplete).toHaveBeenCalled();
});
it('should render custom field extensions properly', async () => {
const MockComponent = () => {
return <h1>im a custom field extension</h1>;
@@ -26,9 +26,9 @@ import {
Button,
makeStyles,
} from '@material-ui/core';
import { withTheme } from '@rjsf/core-v5';
import { type IChangeEvent, withTheme } from '@rjsf/core-v5';
import { ErrorSchema, FieldValidation } from '@rjsf/utils';
import React, { useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { NextFieldExtensionOptions } from '../../../extensions';
import { TemplateParameterSchema } from '../../../types';
import { createAsyncValidators } from './createAsyncValidators';
@@ -36,7 +36,6 @@ import { useTemplateSchema } from './useTemplateSchema';
import { ReviewState } from './ReviewState';
import validator from '@rjsf/validator-ajv6';
import { selectedTemplateRouteRef } from '../../../routes';
import { getDefaultFormState } from '@rjsf/utils';
import { useFormData } from './useFormData';
import { FormProps } from '../../types';
@@ -102,6 +101,12 @@ export const Stepper = (props: StepperProps) => {
setActiveStep(prevActiveStep => prevActiveStep - 1);
};
const handleChange = useCallback(
(e: IChangeEvent) =>
setFormState(current => ({ ...current, ...e.formData })),
[setFormState],
);
const handleNext = async ({
formData,
}: {
@@ -111,18 +116,7 @@ export const Stepper = (props: StepperProps) => {
// to display it's own loading? Or should we grey out the entire form.
setErrors(undefined);
const schema = steps[activeStep]?.schema;
const rootSchema = steps[activeStep]?.mergedSchema;
const newFormData = getDefaultFormState(
validator,
schema,
formData,
rootSchema,
true,
);
const returnedValidation = await validation(newFormData);
const returnedValidation = await validation(formData);
const hasErrors = Object.values(returnedValidation).some(
i => i.__errors?.length,
@@ -138,7 +132,7 @@ export const Stepper = (props: StepperProps) => {
return stepNum;
});
}
setFormState(current => ({ ...current, ...newFormData }));
setFormState(current => ({ ...current, ...formData }));
};
return (
@@ -165,6 +159,7 @@ export const Stepper = (props: StepperProps) => {
onSubmit={handleNext}
fields={extensions}
showErrorList={false}
onChange={handleChange}
{...(props.FormProps ?? {})}
>
<div className={styles.footer}>
@@ -250,4 +250,33 @@ describe('ElasticSearchSearchEngineIndexer', () => {
// Final deletion shouldn't be called.
expect(deleteSpy).not.toHaveBeenCalled();
});
it('handles bulk client rejection', async () => {
// Given an ES client wrapper that rejects an error
const expectedError = new Error('HTTP Timeout');
const mockClientWrapper = ElasticSearchClientWrapper.fromClientOptions({
node: 'http://localhost:9200',
Connection: mock.getConnection(),
});
mockClientWrapper.bulk = jest.fn().mockRejectedValue(expectedError);
// And a search engine indexer that uses that client wrapper
indexer = new ElasticSearchSearchEngineIndexer({
type: 'some-type',
indexPrefix: '',
indexSeparator: '-index__',
alias: 'some-type-index__search',
logger: getVoidLogger(),
elasticSearchClientWrapper: mockClientWrapper,
batchSize: 1000,
});
// When the indexer is run in the test pipeline
const { error } = await TestPipeline.fromIndexer(indexer)
.withDocuments([{ title: 'a', location: 'a', text: '/a' }])
.execute();
// Then the pipeline should have received the expected error
expect(error).toBe(expectedError);
});
});
@@ -45,7 +45,6 @@ function duration(startTimestamp: [number, number]): string {
* @public
*/
export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
private received: number = 0;
private processed: number = 0;
private removableIndices: string[] = [];
@@ -59,10 +58,13 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
private readonly logger: Logger;
private readonly sourceStream: Readable;
private readonly elasticSearchClientWrapper: ElasticSearchClientWrapper;
private configuredBatchSize: number;
private bulkResult: Promise<any>;
private bulkClientError?: Error;
constructor(options: ElasticSearchSearchEngineIndexerOptions) {
super({ batchSize: options.batchSize });
this.configuredBatchSize = options.batchSize;
this.logger = options.logger.child({ documentType: options.type });
this.startTimestamp = process.hrtime();
this.type = options.type;
@@ -94,6 +96,11 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
},
refreshOnCompletion: that.indexName,
});
// Safely catch errors thrown by the bulk helper client, e.g. HTTP timeouts
this.bulkResult.catch(e => {
this.bulkClientError = e;
});
}
async initialize(): Promise<void> {
@@ -115,7 +122,6 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
async index(documents: IndexableDocument[]): Promise<void> {
await this.isReady();
documents.forEach(document => {
this.received++;
this.sourceStream.push(document);
});
}
@@ -197,11 +203,41 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
* backpressure in other parts of the indexing pipeline.
*/
private isReady(): Promise<void> {
return new Promise(resolve => {
// Early exit if the underlying ES client encountered an error.
if (this.bulkClientError) {
return Promise.reject(this.bulkClientError);
}
// Optimization: if the stream that ES reads from has fewer docs queued
// than the configured batch size, continue early to allow more docs to be
// queued
if (this.sourceStream.readableLength < this.configuredBatchSize) {
return Promise.resolve();
}
// Otherwise, continue periodically checking the stream queue to see if
// ES has consumed the documents and continue when it's ready for more.
return new Promise((isReady, abort) => {
let streamLengthChecks = 0;
const interval = setInterval(() => {
if (this.received === this.processed) {
streamLengthChecks++;
if (this.sourceStream.readableLength < this.configuredBatchSize) {
clearInterval(interval);
resolve();
isReady();
}
// Do not allow this interval to loop endlessly; anything longer than 5
// minutes likely indicates an unrecoverable error in ES; direct the
// user to inspect ES logs for more clues and abort in order to allow
// the index to be cleaned up.
if (streamLengthChecks >= 6000) {
clearInterval(interval);
abort(
new Error(
'Exceeded 5 minutes waiting for elastic to be ready to accept more documents. Check the elastic logs for possible problems.',
),
);
}
}, 50);
});
+102 -102
View File
@@ -408,15 +408,15 @@ __metadata:
languageName: node
linkType: hard
"@aws-sdk/client-cognito-identity@npm:3.235.0":
version: 3.235.0
resolution: "@aws-sdk/client-cognito-identity@npm:3.235.0"
"@aws-sdk/client-cognito-identity@npm:3.236.0":
version: 3.236.0
resolution: "@aws-sdk/client-cognito-identity@npm:3.236.0"
dependencies:
"@aws-crypto/sha256-browser": 2.0.0
"@aws-crypto/sha256-js": 2.0.0
"@aws-sdk/client-sts": 3.235.0
"@aws-sdk/client-sts": 3.236.0
"@aws-sdk/config-resolver": 3.234.0
"@aws-sdk/credential-provider-node": 3.235.0
"@aws-sdk/credential-provider-node": 3.236.0
"@aws-sdk/fetch-http-handler": 3.226.0
"@aws-sdk/hash-node": 3.226.0
"@aws-sdk/invalid-dependency": 3.226.0
@@ -448,20 +448,20 @@ __metadata:
"@aws-sdk/util-utf8-browser": 3.188.0
"@aws-sdk/util-utf8-node": 3.208.0
tslib: ^2.3.1
checksum: 4637132cc1a95823867c5ee80b35c7b84ddeed83c0778c2d7c197b26f24899641c825926c886c293f36d62e6b72be03ee091ac11cc9dbd875ad85936548ee178
checksum: 808339bd0113f65674dea0d65af25c350ff5a8f2595f9dd11215b6e457df5d243db156042dc4bfbb39b302d6f871a71fbbed20f403207fb6e4c9e444eb802bf1
languageName: node
linkType: hard
"@aws-sdk/client-s3@npm:^3.208.0":
version: 3.235.0
resolution: "@aws-sdk/client-s3@npm:3.235.0"
version: 3.236.0
resolution: "@aws-sdk/client-s3@npm:3.236.0"
dependencies:
"@aws-crypto/sha1-browser": 2.0.0
"@aws-crypto/sha256-browser": 2.0.0
"@aws-crypto/sha256-js": 2.0.0
"@aws-sdk/client-sts": 3.235.0
"@aws-sdk/client-sts": 3.236.0
"@aws-sdk/config-resolver": 3.234.0
"@aws-sdk/credential-provider-node": 3.235.0
"@aws-sdk/credential-provider-node": 3.236.0
"@aws-sdk/eventstream-serde-browser": 3.226.0
"@aws-sdk/eventstream-serde-config-resolver": 3.226.0
"@aws-sdk/eventstream-serde-node": 3.226.0
@@ -511,19 +511,19 @@ __metadata:
"@aws-sdk/xml-builder": 3.201.0
fast-xml-parser: 4.0.11
tslib: ^2.3.1
checksum: b16ce0a34a4d302edbc9cae91296a90a907511d6f0985feccf2196d537e1f1bfff6bfc0dafcb7ea1d5d7d0afd79d843cc15f8a5f87debd08852de338c698169b
checksum: 6a8953c229a1824bf1639dfa1a593bff3419c2c338da8df956d5cd3098dc3c1236ee63564347a22f40dc5947322b3f315ae7e39dd9c115ccf8a28199168c6d3b
languageName: node
linkType: hard
"@aws-sdk/client-sqs@npm:^3.208.0":
version: 3.235.0
resolution: "@aws-sdk/client-sqs@npm:3.235.0"
version: 3.236.0
resolution: "@aws-sdk/client-sqs@npm:3.236.0"
dependencies:
"@aws-crypto/sha256-browser": 2.0.0
"@aws-crypto/sha256-js": 2.0.0
"@aws-sdk/client-sts": 3.235.0
"@aws-sdk/client-sts": 3.236.0
"@aws-sdk/config-resolver": 3.234.0
"@aws-sdk/credential-provider-node": 3.235.0
"@aws-sdk/credential-provider-node": 3.236.0
"@aws-sdk/fetch-http-handler": 3.226.0
"@aws-sdk/hash-node": 3.226.0
"@aws-sdk/invalid-dependency": 3.226.0
@@ -558,13 +558,13 @@ __metadata:
"@aws-sdk/util-utf8-node": 3.208.0
fast-xml-parser: 4.0.11
tslib: ^2.3.1
checksum: 04da2ab0a850fa74265b9ab91b14fdf470d7f2850e31866b3582b83a01991bd9555b370e34bceba2445910b2d60cf8e17d7ead1f5caa94987e4114db949548f8
checksum: c1d3eb3886d8d799afd081cf76822627e359495daa94edb615a001704933b31d276249828c5e5823af49ba08826875a4c768d8e6b4493abe9b65583a585839b7
languageName: node
linkType: hard
"@aws-sdk/client-sso-oidc@npm:3.235.0":
version: 3.235.0
resolution: "@aws-sdk/client-sso-oidc@npm:3.235.0"
"@aws-sdk/client-sso-oidc@npm:3.236.0":
version: 3.236.0
resolution: "@aws-sdk/client-sso-oidc@npm:3.236.0"
dependencies:
"@aws-crypto/sha256-browser": 2.0.0
"@aws-crypto/sha256-js": 2.0.0
@@ -599,13 +599,13 @@ __metadata:
"@aws-sdk/util-utf8-browser": 3.188.0
"@aws-sdk/util-utf8-node": 3.208.0
tslib: ^2.3.1
checksum: c62558375fb6d3ce44136019de99e5134805894a80cec191e145003c0e065eb52ba0a288a62884371d6a69370f92df522b120e1c5428520c09ba0f4cdc5b91b7
checksum: 6fbbebb37547bafc9345c1fd03692728d6b6015f7583f22b2542beaac7f395ab29af884f1a353e78dd147ebf8b109b1d6678f9d1ae18c2b70739e9ceee6781ef
languageName: node
linkType: hard
"@aws-sdk/client-sso@npm:3.235.0":
version: 3.235.0
resolution: "@aws-sdk/client-sso@npm:3.235.0"
"@aws-sdk/client-sso@npm:3.236.0":
version: 3.236.0
resolution: "@aws-sdk/client-sso@npm:3.236.0"
dependencies:
"@aws-crypto/sha256-browser": 2.0.0
"@aws-crypto/sha256-js": 2.0.0
@@ -640,18 +640,18 @@ __metadata:
"@aws-sdk/util-utf8-browser": 3.188.0
"@aws-sdk/util-utf8-node": 3.208.0
tslib: ^2.3.1
checksum: 9852704cc383cbc8e44013eed6de514f60dc4f70ab89ceea79971b4b286db6b923648e8e4169d248f0888efed19c7c5fcc5748177577635d07080fd4980431d7
checksum: 15cb25a3c12fe0f11ed320c8f02f72ee0d43291a14c06fce81f608fbc75a85785698fbffc3112e7e92ece25460d24670ba345535b43003b20798a0fcc8a7b31d
languageName: node
linkType: hard
"@aws-sdk/client-sts@npm:3.235.0, @aws-sdk/client-sts@npm:^3.208.0":
version: 3.235.0
resolution: "@aws-sdk/client-sts@npm:3.235.0"
"@aws-sdk/client-sts@npm:3.236.0, @aws-sdk/client-sts@npm:^3.208.0":
version: 3.236.0
resolution: "@aws-sdk/client-sts@npm:3.236.0"
dependencies:
"@aws-crypto/sha256-browser": 2.0.0
"@aws-crypto/sha256-js": 2.0.0
"@aws-sdk/config-resolver": 3.234.0
"@aws-sdk/credential-provider-node": 3.235.0
"@aws-sdk/credential-provider-node": 3.236.0
"@aws-sdk/fetch-http-handler": 3.226.0
"@aws-sdk/hash-node": 3.226.0
"@aws-sdk/invalid-dependency": 3.226.0
@@ -685,7 +685,7 @@ __metadata:
"@aws-sdk/util-utf8-node": 3.208.0
fast-xml-parser: 4.0.11
tslib: ^2.3.1
checksum: 9a2ec3240bd01fd1ed95b2c6766ea8d141e4b85a6535fcbb5c57ac725ef58c2d1c1403a61c336adbb1eceb5dd306ac1d35d7b8f25ed1ebc544a14ad14fb038a7
checksum: 98463fe50a588032b82be5289445c6e7250bd947076e5981ba7c126386d567f3b1ea9a9652c032da919503cdba2c3b517061911ddffae0c91f880f7eabaeaa08
languageName: node
linkType: hard
@@ -702,15 +702,15 @@ __metadata:
languageName: node
linkType: hard
"@aws-sdk/credential-provider-cognito-identity@npm:3.235.0":
version: 3.235.0
resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.235.0"
"@aws-sdk/credential-provider-cognito-identity@npm:3.236.0":
version: 3.236.0
resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.236.0"
dependencies:
"@aws-sdk/client-cognito-identity": 3.235.0
"@aws-sdk/client-cognito-identity": 3.236.0
"@aws-sdk/property-provider": 3.226.0
"@aws-sdk/types": 3.226.0
tslib: ^2.3.1
checksum: 85d348faa5161282d226040315aaff1a754d3bbd23ccb9131f021a5f6a473be76b80e2b3f5132d3b0a119732822d6b76829626d64e6c71c4d61d1fcbeb6e0080
checksum: 29d3178f490cbb711553103c0fb544604c5e70e36395f1edc6b9bd2ee0e928160b911b4893fe89418c182548d0dddfbd44e9ef629ad690ab1724a2a693368c16
languageName: node
linkType: hard
@@ -738,38 +738,38 @@ __metadata:
languageName: node
linkType: hard
"@aws-sdk/credential-provider-ini@npm:3.235.0":
version: 3.235.0
resolution: "@aws-sdk/credential-provider-ini@npm:3.235.0"
"@aws-sdk/credential-provider-ini@npm:3.236.0":
version: 3.236.0
resolution: "@aws-sdk/credential-provider-ini@npm:3.236.0"
dependencies:
"@aws-sdk/credential-provider-env": 3.226.0
"@aws-sdk/credential-provider-imds": 3.226.0
"@aws-sdk/credential-provider-process": 3.226.0
"@aws-sdk/credential-provider-sso": 3.235.0
"@aws-sdk/credential-provider-sso": 3.236.0
"@aws-sdk/credential-provider-web-identity": 3.226.0
"@aws-sdk/property-provider": 3.226.0
"@aws-sdk/shared-ini-file-loader": 3.226.0
"@aws-sdk/types": 3.226.0
tslib: ^2.3.1
checksum: c910746d90ec363691dc99d53594beb49ea8562ef69c5a2e9a1702febc7d870cfe5b0f441afdae2bed643a4e8901b82f965660cdd70077bb006978fdafddeb05
checksum: e748aba37dab11ed395fa60c7eee119b997e4c64b1a838c93258cfe6a6e289dfa0aa20d292b94d06da8148b7cb32604b24f84aadc2a389bf8248c64d80ef44d5
languageName: node
linkType: hard
"@aws-sdk/credential-provider-node@npm:3.235.0, @aws-sdk/credential-provider-node@npm:^3.208.0":
version: 3.235.0
resolution: "@aws-sdk/credential-provider-node@npm:3.235.0"
"@aws-sdk/credential-provider-node@npm:3.236.0, @aws-sdk/credential-provider-node@npm:^3.208.0":
version: 3.236.0
resolution: "@aws-sdk/credential-provider-node@npm:3.236.0"
dependencies:
"@aws-sdk/credential-provider-env": 3.226.0
"@aws-sdk/credential-provider-imds": 3.226.0
"@aws-sdk/credential-provider-ini": 3.235.0
"@aws-sdk/credential-provider-ini": 3.236.0
"@aws-sdk/credential-provider-process": 3.226.0
"@aws-sdk/credential-provider-sso": 3.235.0
"@aws-sdk/credential-provider-sso": 3.236.0
"@aws-sdk/credential-provider-web-identity": 3.226.0
"@aws-sdk/property-provider": 3.226.0
"@aws-sdk/shared-ini-file-loader": 3.226.0
"@aws-sdk/types": 3.226.0
tslib: ^2.3.1
checksum: bfea379a5e66c311c57a1f7390b2e508d42f8cf3e1a48a5b9585b33c76339c7e8806dba413c7d5bf6d669ffd05a4c55ef705f92bf4687ba8f5e359c5d3c7495e
checksum: d5a26b40a985edfef5006efde921d58b78829b67eed9fea56e108b99eec437a02721b93ef0e2dcd9a4739716c85d8db75935a71d2afa1190b0f1d0010511f649
languageName: node
linkType: hard
@@ -785,17 +785,17 @@ __metadata:
languageName: node
linkType: hard
"@aws-sdk/credential-provider-sso@npm:3.235.0":
version: 3.235.0
resolution: "@aws-sdk/credential-provider-sso@npm:3.235.0"
"@aws-sdk/credential-provider-sso@npm:3.236.0":
version: 3.236.0
resolution: "@aws-sdk/credential-provider-sso@npm:3.236.0"
dependencies:
"@aws-sdk/client-sso": 3.235.0
"@aws-sdk/client-sso": 3.236.0
"@aws-sdk/property-provider": 3.226.0
"@aws-sdk/shared-ini-file-loader": 3.226.0
"@aws-sdk/token-providers": 3.235.0
"@aws-sdk/token-providers": 3.236.0
"@aws-sdk/types": 3.226.0
tslib: ^2.3.1
checksum: 358fdda689c1f9be768516ca39de17e7fa7832d3d89d8893dd58f5d3377908ee2580155c1ba984c5ed21a410cda681bd22c7c74876af06b3496641a98b62353f
checksum: 215e0cd08c4dca453a80ce695bf4dddb94ce7562100bfa64e2ba5406ab3aeb6cf92882bd714b6ee95d3f13c45f3286eb616d32e1ffc58728f1e21e7eb171b301
languageName: node
linkType: hard
@@ -811,25 +811,25 @@ __metadata:
linkType: hard
"@aws-sdk/credential-providers@npm:^3.208.0":
version: 3.235.0
resolution: "@aws-sdk/credential-providers@npm:3.235.0"
version: 3.236.0
resolution: "@aws-sdk/credential-providers@npm:3.236.0"
dependencies:
"@aws-sdk/client-cognito-identity": 3.235.0
"@aws-sdk/client-sso": 3.235.0
"@aws-sdk/client-sts": 3.235.0
"@aws-sdk/credential-provider-cognito-identity": 3.235.0
"@aws-sdk/client-cognito-identity": 3.236.0
"@aws-sdk/client-sso": 3.236.0
"@aws-sdk/client-sts": 3.236.0
"@aws-sdk/credential-provider-cognito-identity": 3.236.0
"@aws-sdk/credential-provider-env": 3.226.0
"@aws-sdk/credential-provider-imds": 3.226.0
"@aws-sdk/credential-provider-ini": 3.235.0
"@aws-sdk/credential-provider-node": 3.235.0
"@aws-sdk/credential-provider-ini": 3.236.0
"@aws-sdk/credential-provider-node": 3.236.0
"@aws-sdk/credential-provider-process": 3.226.0
"@aws-sdk/credential-provider-sso": 3.235.0
"@aws-sdk/credential-provider-sso": 3.236.0
"@aws-sdk/credential-provider-web-identity": 3.226.0
"@aws-sdk/property-provider": 3.226.0
"@aws-sdk/shared-ini-file-loader": 3.226.0
"@aws-sdk/types": 3.226.0
tslib: ^2.3.1
checksum: 3c8436f78a4ddcf295b06d77ed3cb912a8ae56ee047526663f8316dde6b646095c8288480214c9e3f68cb98772511cb7fcec6f8c4100fba899c495a9429c3ba2
checksum: 3105661946fa12322b18a2227f26a8b734072850882e771493b398e714b433c024873a4495a18d55a4213022b07c658cb7ed38859b5fa152fc2ce1e02b408ca1
languageName: node
linkType: hard
@@ -967,8 +967,8 @@ __metadata:
linkType: hard
"@aws-sdk/lib-storage@npm:^3.208.0":
version: 3.235.0
resolution: "@aws-sdk/lib-storage@npm:3.235.0"
version: 3.236.0
resolution: "@aws-sdk/lib-storage@npm:3.236.0"
dependencies:
"@aws-sdk/middleware-endpoint": 3.226.0
"@aws-sdk/smithy-client": 3.234.0
@@ -979,7 +979,7 @@ __metadata:
peerDependencies:
"@aws-sdk/abort-controller": ^3.0.0
"@aws-sdk/client-s3": ^3.0.0
checksum: a9a8993fdc8c05769a2f0c5c838e9b2ba72216f234e67c1a8bcebf8f812ab397d48c8b118959f4d9918ad8811fdf93b3b47812075b0995af8c11cb26c0f3c305
checksum: 783b67c4605a4f7d4072ce38b2b2416a0a6771784f9ba5b1608be3777e7d21a18c820fff94218856607f984c453376e25e80393ef233365476850e354f8e15f8
languageName: node
linkType: hard
@@ -1355,16 +1355,16 @@ __metadata:
languageName: node
linkType: hard
"@aws-sdk/token-providers@npm:3.235.0":
version: 3.235.0
resolution: "@aws-sdk/token-providers@npm:3.235.0"
"@aws-sdk/token-providers@npm:3.236.0":
version: 3.236.0
resolution: "@aws-sdk/token-providers@npm:3.236.0"
dependencies:
"@aws-sdk/client-sso-oidc": 3.235.0
"@aws-sdk/client-sso-oidc": 3.236.0
"@aws-sdk/property-provider": 3.226.0
"@aws-sdk/shared-ini-file-loader": 3.226.0
"@aws-sdk/types": 3.226.0
tslib: ^2.3.1
checksum: 5b9cd8cfd697f04a7c628329af1cc5003ec25fe08626148c55389311b53ecbaa47b83f2c6b832fa847f9db2ca5a3f63bcb8a1059b1b3b3590010acf3d4610896
checksum: c5e6c88f22d35002f2d575d23ba5a530ee195b7220d7c0c58603bc2fafb6632a6755bc863cb7519eadda110d2f3193a144f34cc0236df285fcde40c980ba653a
languageName: node
linkType: hard
@@ -8231,9 +8231,6 @@ __metadata:
"@manypkg/get-packages": ^1.1.3
"@microsoft/api-documenter": ^7.17.11
"@microsoft/api-extractor": ^7.23.0
"@microsoft/api-extractor-model": ^7.17.2
"@microsoft/tsdoc": 0.14.1
"@microsoft/tsdoc-config": 0.16.2
"@types/is-glob": ^4.0.2
"@types/mock-fs": ^4.13.0
chalk: ^4.0.0
@@ -8245,6 +8242,9 @@ __metadata:
mock-fs: ^5.1.0
ts-node: ^10.0.0
peerDependencies:
"@microsoft/api-extractor-model": "*"
"@microsoft/tsdoc": "*"
"@microsoft/tsdoc-config": "*"
"@rushstack/node-core-library": "*"
prettier: ^2.8.1
typescript: "> 3.0.0"
@@ -9019,20 +9019,20 @@ __metadata:
languageName: node
linkType: hard
"@eslint/eslintrc@npm:^1.3.3":
version: 1.3.3
resolution: "@eslint/eslintrc@npm:1.3.3"
"@eslint/eslintrc@npm:^1.4.0":
version: 1.4.0
resolution: "@eslint/eslintrc@npm:1.4.0"
dependencies:
ajv: ^6.12.4
debug: ^4.3.2
espree: ^9.4.0
globals: ^13.15.0
globals: ^13.19.0
ignore: ^5.2.0
import-fresh: ^3.2.1
js-yaml: ^4.1.0
minimatch: ^3.1.2
strip-json-comments: ^3.1.1
checksum: f03e9d6727efd3e0719da2051ea80c0c73d20e28c171121527dbb868cd34232ca9c1d0525a66e517a404afea26624b1e47895b6a92474678418c2f50c9566694
checksum: 73e39c833deafde8d8706e6fa9b52b6d99927c094ead8e405ea4174e8197ec24aac9ba88ae38cc8ad32eaccf07b9c7fc5dc70761d1fba6da41a928691447305f
languageName: node
linkType: hard
@@ -9853,14 +9853,14 @@ __metadata:
languageName: node
linkType: hard
"@humanwhocodes/config-array@npm:^0.11.6":
version: 0.11.7
resolution: "@humanwhocodes/config-array@npm:0.11.7"
"@humanwhocodes/config-array@npm:^0.11.8":
version: 0.11.8
resolution: "@humanwhocodes/config-array@npm:0.11.8"
dependencies:
"@humanwhocodes/object-schema": ^1.2.1
debug: ^4.1.1
minimatch: ^3.0.5
checksum: cf506dc45d9488af7fbf108ea6ac2151ba1a25e6d2b94b9b4fc36d2c1e4099b89ff560296dbfa13947e44604d4ca4a90d97a4fb167370bf8dd01a6ca2b6d83ac
checksum: 0fd6b3c54f1674ce0a224df09b9c2f9846d20b9e54fabae1281ecfc04f2e6ad69bf19e1d6af6a28f88e8aa3990168b6cb9e1ef755868c3256a630605ec2cb1d3
languageName: node
linkType: hard
@@ -11235,7 +11235,7 @@ __metadata:
languageName: node
linkType: hard
"@microsoft/api-extractor-model@npm:7.17.2, @microsoft/api-extractor-model@npm:^7.17.2":
"@microsoft/api-extractor-model@npm:7.17.2":
version: 7.17.2
resolution: "@microsoft/api-extractor-model@npm:7.17.2"
dependencies:
@@ -11275,7 +11275,7 @@ __metadata:
languageName: node
linkType: hard
"@microsoft/tsdoc-config@npm:0.16.2, @microsoft/tsdoc-config@npm:~0.16.1":
"@microsoft/tsdoc-config@npm:~0.16.1":
version: 0.16.2
resolution: "@microsoft/tsdoc-config@npm:0.16.2"
dependencies:
@@ -12470,8 +12470,8 @@ __metadata:
linkType: hard
"@rjsf/utils@npm:^5.0.0-beta.14":
version: 5.0.0-beta.14
resolution: "@rjsf/utils@npm:5.0.0-beta.14"
version: 5.0.0-beta.15
resolution: "@rjsf/utils@npm:5.0.0-beta.15"
dependencies:
json-schema-merge-allof: ^0.8.1
jsonpointer: ^5.0.1
@@ -12480,20 +12480,20 @@ __metadata:
react-is: ^18.2.0
peerDependencies:
react: ^16.14.0 || >=17
checksum: 66d29377c98f6e778ec0b167734a949528b0ca2bfb4776dea74fd70add1fb339e14ec878b9162a152e83804bc8094c3ebcd9ebf8b1b68a63c6ee2bf4548938c8
checksum: e22b1cc13183d6a6a57b900bb80825eff1c4680edadb554adb6737c21237c7862b2a249e5b67c0c6971094b5d21224daf108ad1f400286be924bbc5c29784a00
languageName: node
linkType: hard
"@rjsf/validator-ajv6@npm:^5.0.0-beta.14":
version: 5.0.0-beta.14
resolution: "@rjsf/validator-ajv6@npm:5.0.0-beta.14"
version: 5.0.0-beta.15
resolution: "@rjsf/validator-ajv6@npm:5.0.0-beta.15"
dependencies:
ajv: ^6.7.0
lodash: ^4.17.15
lodash-es: ^4.17.15
peerDependencies:
"@rjsf/utils": ^5.0.0-beta.1
checksum: 57b5c6968475a66762f644f2832aad077f8dde0ca9e71c8b0b27c06174855b7e6330f44fa6078fc0f347b728a4a5fd62399378fad26ada3cde32da89bddf6f9f
checksum: ce6d2cd110eee21e37ae08e4081b0f0fd65ac22f8c94ceeaff9f9a9482072897c3f988de1bc770c25031932117e7f71dda0bd166c2bb953af420d0ce1fd255d0
languageName: node
linkType: hard
@@ -17866,14 +17866,14 @@ __metadata:
linkType: hard
"canvas@npm:^2.10.2":
version: 2.10.2
resolution: "canvas@npm:2.10.2"
version: 2.11.0
resolution: "canvas@npm:2.11.0"
dependencies:
"@mapbox/node-pre-gyp": ^1.0.0
nan: ^2.17.0
node-gyp: latest
simple-get: ^3.0.3
checksum: b2e3eb4c3635fa2f67857619621c3d314f935a9e51904536dadf4908ab580dff4f5bcbaafe6eb0255247fa027ca494d5cd97c33376a49a0f994997263fa9944b
checksum: a69a6e0c90014a1b02e52c4c38a627d1a01ffe9539047bec84105cb3554907a947cf39b4a333be43fc1583dd142b641bb5480a4e23f59c6098618c33bf78f67f
languageName: node
linkType: hard
@@ -21670,11 +21670,11 @@ __metadata:
linkType: hard
"eslint@npm:^8.6.0":
version: 8.29.0
resolution: "eslint@npm:8.29.0"
version: 8.30.0
resolution: "eslint@npm:8.30.0"
dependencies:
"@eslint/eslintrc": ^1.3.3
"@humanwhocodes/config-array": ^0.11.6
"@eslint/eslintrc": ^1.4.0
"@humanwhocodes/config-array": ^0.11.8
"@humanwhocodes/module-importer": ^1.0.1
"@nodelib/fs.walk": ^1.2.8
ajv: ^6.10.0
@@ -21693,7 +21693,7 @@ __metadata:
file-entry-cache: ^6.0.1
find-up: ^5.0.0
glob-parent: ^6.0.2
globals: ^13.15.0
globals: ^13.19.0
grapheme-splitter: ^1.0.4
ignore: ^5.2.0
import-fresh: ^3.0.0
@@ -21714,7 +21714,7 @@ __metadata:
text-table: ^0.2.0
bin:
eslint: bin/eslint.js
checksum: e05204b05907b82d910983995cb946e0ba62ca514eb2b6791c43f623333b143564a2eee0139909d31c10935c21877d815b1f76dd674a59cb91c471064325c4ab
checksum: b7525bb465b342665c3b8bab7e114d514ef1bc4e79f211c919863f9c71767e7412ec82383a22614a92d159783f91101018817000f7c61ce69a5e7015280cafaf
languageName: node
linkType: hard
@@ -23516,12 +23516,12 @@ __metadata:
languageName: node
linkType: hard
"globals@npm:^13.15.0":
version: 13.15.0
resolution: "globals@npm:13.15.0"
"globals@npm:^13.19.0":
version: 13.19.0
resolution: "globals@npm:13.19.0"
dependencies:
type-fest: ^0.20.2
checksum: 383ade0873b2ab29ce6d143466c203ed960491575bc97406395e5c8434026fb02472ab2dfff5bc16689b8460269b18fda1047975295cd0183904385c51258bae
checksum: a000dbd00bcf28f0941d8a29c3522b1c3b8e4bfe4e60e262c477a550c3cbbe8dbe2925a6905f037acd40f9a93c039242e1f7079c76b0fd184bc41dcc3b5c8e2e
languageName: node
linkType: hard