From caa9d00ac4d54047a3e4dc06555cfba443c84605 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 10:39:37 +0000 Subject: [PATCH 01/90] chore(deps): update dependency prettier to v2.8.2 Signed-off-by: Renovate Bot --- microsite-next/yarn.lock | 6 +++--- microsite/yarn.lock | 6 +++--- yarn.lock | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/microsite-next/yarn.lock b/microsite-next/yarn.lock index 3a632b3079..54b353b415 100644 --- a/microsite-next/yarn.lock +++ b/microsite-next/yarn.lock @@ -8361,11 +8361,11 @@ __metadata: linkType: hard "prettier@npm:^2.6.2": - version: 2.8.1 - resolution: "prettier@npm:2.8.1" + version: 2.8.2 + resolution: "prettier@npm:2.8.2" bin: prettier: bin-prettier.js - checksum: 4f21a0f1269f76fb36f54e9a8a1ea4c11e27478958bf860661fb4b6d7ac69aac1581f8724fa98ea3585e56d42a2ea317a17ff6e3324f40cb11ff9e20b73785cc + checksum: 740c56c2128d587d656ea1dde9bc9c3503dfc94db4f3ac387259215eeb2e216680bdad9d18a0c9feecc6b42cfa188d6fa777df4c36c1d00cedd4199074fbfbd2 languageName: node linkType: hard diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 3bc81619e3..34ed76e9e4 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -7369,11 +7369,11 @@ __metadata: linkType: hard "prettier@npm:^2.6.2": - version: 2.8.1 - resolution: "prettier@npm:2.8.1" + version: 2.8.2 + resolution: "prettier@npm:2.8.2" bin: prettier: bin-prettier.js - checksum: 4f21a0f1269f76fb36f54e9a8a1ea4c11e27478958bf860661fb4b6d7ac69aac1581f8724fa98ea3585e56d42a2ea317a17ff6e3324f40cb11ff9e20b73785cc + checksum: 740c56c2128d587d656ea1dde9bc9c3503dfc94db4f3ac387259215eeb2e216680bdad9d18a0c9feecc6b42cfa188d6fa777df4c36c1d00cedd4199074fbfbd2 languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index 56fa82473c..fc273856e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32407,11 +32407,11 @@ __metadata: linkType: hard "prettier@npm:^2.2.1, prettier@npm:^2.7.1": - version: 2.8.1 - resolution: "prettier@npm:2.8.1" + version: 2.8.2 + resolution: "prettier@npm:2.8.2" bin: prettier: bin-prettier.js - checksum: 4f21a0f1269f76fb36f54e9a8a1ea4c11e27478958bf860661fb4b6d7ac69aac1581f8724fa98ea3585e56d42a2ea317a17ff6e3324f40cb11ff9e20b73785cc + checksum: 740c56c2128d587d656ea1dde9bc9c3503dfc94db4f3ac387259215eeb2e216680bdad9d18a0c9feecc6b42cfa188d6fa777df4c36c1d00cedd4199074fbfbd2 languageName: node linkType: hard From 5c1abc3bbfda6be79fbfb002c93c96b71154e771 Mon Sep 17 00:00:00 2001 From: Mark Anderson-Trocme Date: Mon, 16 Jan 2023 14:34:09 -0500 Subject: [PATCH 02/90] Feature: Make workflow a link in CircleCI plugin Signed-off-by: Mark Anderson-Trocme --- .../src/components/BuildsPage/lib/CITable/CITable.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx index adc8a569bb..8f99f1f69c 100644 --- a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -212,6 +212,10 @@ const generatedColumns: TableColumn[] = [ { title: 'Workflow', field: 'workflow.name', + highlight: true, + render: (row: Partial) => ( + {row?.workflow?.name} + ), }, { title: 'Actions', From 1616e06047ebd78e2904e93cfaf9264a6479a3c2 Mon Sep 17 00:00:00 2001 From: Mark Anderson-Trocme Date: Mon, 16 Jan 2023 14:52:47 -0500 Subject: [PATCH 03/90] add changeset Signed-off-by: Mark Anderson-Trocme --- .changeset/slow-moles-sin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slow-moles-sin.md diff --git a/.changeset/slow-moles-sin.md b/.changeset/slow-moles-sin.md new file mode 100644 index 0000000000..13527b509e --- /dev/null +++ b/.changeset/slow-moles-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-circleci': minor +--- + +Making workflow a link From cbe69fa263663b93276e0f17b140208093a66732 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 27 Jan 2023 12:05:53 +0100 Subject: [PATCH 04/90] fix formatting Signed-off-by: Patrik Oldsberg --- packages/backend-common/src/scm/git.test.ts | 6 +++--- .../src/layout/InfoCard/InfoCard.tsx | 4 ++-- packages/techdocs-cli/src/lib/PublisherConfig.ts | 4 ++-- plugins/badges-backend/src/types.ts | 2 +- plugins/bitbucket-cloud-common/src/models/index.ts | 14 +++++++------- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/backend-common/src/scm/git.test.ts b/packages/backend-common/src/scm/git.test.ts index fdd3c7dbc8..629173f96e 100644 --- a/packages/backend-common/src/scm/git.test.ts +++ b/packages/backend-common/src/scm/git.test.ts @@ -202,7 +202,7 @@ describe('Git', () => { await git.clone({ url, dir }); const { onAuth } = ( - isomorphic.clone as unknown as jest.Mock + isomorphic.clone as unknown as jest.Mock<(typeof isomorphic)['clone']> ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); @@ -315,7 +315,7 @@ describe('Git', () => { await git.fetch({ remote, dir }); const { onAuth } = ( - isomorphic.fetch as unknown as jest.Mock + isomorphic.fetch as unknown as jest.Mock<(typeof isomorphic)['fetch']> ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); @@ -486,7 +486,7 @@ describe('Git', () => { await git.push({ remote, dir, remoteRef, force }); const { onAuth } = ( - isomorphic.push as unknown as jest.Mock + isomorphic.push as unknown as jest.Mock<(typeof isomorphic)['push']> ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); diff --git a/packages/core-components/src/layout/InfoCard/InfoCard.tsx b/packages/core-components/src/layout/InfoCard/InfoCard.tsx index 95d474aae4..3d8d0f3bb0 100644 --- a/packages/core-components/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core-components/src/layout/InfoCard/InfoCard.tsx @@ -188,12 +188,12 @@ export function InfoCard(props: Props): JSX.Element { variants.forEach(name => { calculatedStyle = { ...calculatedStyle, - ...VARIANT_STYLES.card[name as keyof typeof VARIANT_STYLES['card']], + ...VARIANT_STYLES.card[name as keyof (typeof VARIANT_STYLES)['card']], }; calculatedCardStyle = { ...calculatedCardStyle, ...VARIANT_STYLES.cardContent[ - name as keyof typeof VARIANT_STYLES['cardContent'] + name as keyof (typeof VARIANT_STYLES)['cardContent'] ], }; }); diff --git a/packages/techdocs-cli/src/lib/PublisherConfig.ts b/packages/techdocs-cli/src/lib/PublisherConfig.ts index 0d1c8fc8a9..0ef6b66eaf 100644 --- a/packages/techdocs-cli/src/lib/PublisherConfig.ts +++ b/packages/techdocs-cli/src/lib/PublisherConfig.ts @@ -17,7 +17,7 @@ import { ConfigReader } from '@backstage/config'; import { OptionValues } from 'commander'; -type Publisher = keyof typeof PublisherConfig['configFactories']; +type Publisher = keyof (typeof PublisherConfig)['configFactories']; type PublisherConfiguration = { [p in Publisher]?: any; } & { @@ -73,7 +73,7 @@ export class PublisherConfig { */ private static isKnownPublisher( type: string, - ): type is keyof typeof PublisherConfig['configFactories'] { + ): type is keyof (typeof PublisherConfig)['configFactories'] { return PublisherConfig.configFactories.hasOwnProperty(type); } diff --git a/plugins/badges-backend/src/types.ts b/plugins/badges-backend/src/types.ts index eec7063f88..0d778289f9 100644 --- a/plugins/badges-backend/src/types.ts +++ b/plugins/badges-backend/src/types.ts @@ -27,7 +27,7 @@ export const BADGE_STYLES = [ ] as const; /** @public */ -export type BadgeStyle = typeof BADGE_STYLES[number]; +export type BadgeStyle = (typeof BADGE_STYLES)[number]; /** @public */ export interface Badge { diff --git a/plugins/bitbucket-cloud-common/src/models/index.ts b/plugins/bitbucket-cloud-common/src/models/index.ts index 9d25bbb873..d868e2926c 100644 --- a/plugins/bitbucket-cloud-common/src/models/index.ts +++ b/plugins/bitbucket-cloud-common/src/models/index.ts @@ -107,7 +107,7 @@ export namespace Models { * @public */ export type BaseCommitSummaryMarkupEnum = - typeof BaseCommitSummaryMarkupEnum[keyof typeof BaseCommitSummaryMarkupEnum]; + (typeof BaseCommitSummaryMarkupEnum)[keyof typeof BaseCommitSummaryMarkupEnum]; /** * A branch object, representing a branch in a repository. @@ -146,7 +146,7 @@ export namespace Models { * @public */ export type BranchMergeStrategiesEnum = - typeof BranchMergeStrategiesEnum[keyof typeof BranchMergeStrategiesEnum]; + (typeof BranchMergeStrategiesEnum)[keyof typeof BranchMergeStrategiesEnum]; /** * A repository commit object. @@ -191,7 +191,7 @@ export namespace Models { * @public */ export type CommitFileAttributesEnum = - typeof CommitFileAttributesEnum[keyof typeof CommitFileAttributesEnum]; + (typeof CommitFileAttributesEnum)[keyof typeof CommitFileAttributesEnum]; /** * A link to a resource related to this object. @@ -280,7 +280,7 @@ export namespace Models { * @public */ export type ParticipantRoleEnum = - typeof ParticipantRoleEnum[keyof typeof ParticipantRoleEnum]; + (typeof ParticipantRoleEnum)[keyof typeof ParticipantRoleEnum]; /** * @public @@ -295,7 +295,7 @@ export namespace Models { * @public */ export type ParticipantStateEnum = - typeof ParticipantStateEnum[keyof typeof ParticipantStateEnum]; + (typeof ParticipantStateEnum)[keyof typeof ParticipantStateEnum]; /** * A Bitbucket project. @@ -423,7 +423,7 @@ export namespace Models { * @public */ export type RepositoryForkPolicyEnum = - typeof RepositoryForkPolicyEnum[keyof typeof RepositoryForkPolicyEnum]; + (typeof RepositoryForkPolicyEnum)[keyof typeof RepositoryForkPolicyEnum]; /** * @public @@ -436,7 +436,7 @@ export namespace Models { * @public */ export type RepositoryScmEnum = - typeof RepositoryScmEnum[keyof typeof RepositoryScmEnum]; + (typeof RepositoryScmEnum)[keyof typeof RepositoryScmEnum]; /** * @public From 49948f644f1276f19020171a223fa74fbde6d1fd Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Sat, 28 Jan 2023 01:49:58 -0500 Subject: [PATCH 05/90] Standardize term 'gitlab instance' In other places in the docs the term 'instance' is used to refer to a single GitLab API host. The use of 'project' in these cases is particularly troublesome because that term already has a well-defined meaning in GitLab: a single codebase with associated issues, MRs, and automation. Signed-off-by: Jamie Klassen --- .changeset/breezy-ears-rule.md | 5 +++++ docs/integrations/gitlab/discovery.md | 2 +- plugins/catalog-backend-module-gitlab/config.d.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/breezy-ears-rule.md diff --git a/.changeset/breezy-ears-rule.md b/.changeset/breezy-ears-rule.md new file mode 100644 index 0000000000..753554187d --- /dev/null +++ b/.changeset/breezy-ears-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +The config now consistently uses the term 'instance' to refer to a single GitLab API host. diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index dc0b911b3d..51c34b67ac 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -22,7 +22,7 @@ catalog: yourProviderId: host: gitlab-host # Identifies one of the hosts set up in the integrations branch: main # Optional. Uses `master` as default - group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole project will be scanned + group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` projectPattern: /[\s\S]*/ # Optional. Filters found projects based on provided patter. Defaults to `/[\s\S]*/`, what means to not filter anything schedule: # optional; same options as in TaskScheduleDefinition diff --git a/plugins/catalog-backend-module-gitlab/config.d.ts b/plugins/catalog-backend-module-gitlab/config.d.ts index 4f51867efc..00ad0ef032 100644 --- a/plugins/catalog-backend-module-gitlab/config.d.ts +++ b/plugins/catalog-backend-module-gitlab/config.d.ts @@ -31,7 +31,7 @@ export interface Config { host: string; /** * (Optional) Gitlab's group[/subgroup] where the discovery is done. - * If not defined the whole project will be scanned. + * If not defined the whole instance will be scanned. */ group?: string; /** From c5957024a3dc0ced76a9c2c98ed31136f0ca35c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jan 2023 16:41:23 +0000 Subject: [PATCH 06/90] chore(deps): update backstage/actions action to v0.6.0 Signed-off-by: Renovate Bot --- .github/workflows/automate_changeset_feedback.yml | 2 +- .github/workflows/ci.yml | 6 +++--- .github/workflows/cron.yml | 2 +- .github/workflows/deploy_docker-image.yml | 2 +- .github/workflows/deploy_nightly.yml | 2 +- .github/workflows/deploy_packages.yml | 4 ++-- .github/workflows/issue.yaml | 2 +- .github/workflows/pr-review-comment.yaml | 2 +- .github/workflows/pr.yaml | 2 +- .github/workflows/sync_code-formatting.yml | 2 +- .github/workflows/sync_snyk-github-issues.yml | 2 +- .github/workflows/uffizzi-build.yml | 2 +- .github/workflows/verify_e2e-linux.yml | 2 +- .github/workflows/verify_kubernetes.yml | 2 +- .github/workflows/verify_storybook.yml | 2 +- 15 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index e18aa9d428..11fef75b9b 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -28,7 +28,7 @@ jobs: ref: 'refs/pull/${{ github.event.pull_request.number }}/merge' - name: fetch base run: git fetch --depth 1 origin ${{ github.base_ref }} - - uses: backstage/actions/changeset-feedback@v0.5.13 + - uses: backstage/actions/changeset-feedback@v0.6.0 name: Generate feedback with: diffRef: 'origin/master' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10db591f4c..16573cf5b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -63,7 +63,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -181,7 +181,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index dc221c1a1e..2e6573ede8 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -8,7 +8,7 @@ jobs: cron: runs-on: ubuntu-latest steps: - - uses: backstage/actions/cron@v0.5.13 + - uses: backstage/actions/cron@v0.6.0 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index d8470d4124..909a36199c 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -26,7 +26,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index 5f337ace3e..c9cd22640c 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -26,7 +26,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index c05b850883..fa43271a96 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -68,7 +68,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -145,7 +145,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/issue.yaml b/.github/workflows/issue.yaml index d16d0b55d7..9f2e5827be 100644 --- a/.github/workflows/issue.yaml +++ b/.github/workflows/issue.yaml @@ -10,4 +10,4 @@ jobs: if: github.repository == 'backstage/backstage' steps: - name: Issue sync - uses: backstage/actions/issue-sync@v0.5.13 + uses: backstage/actions/issue-sync@v0.6.0 diff --git a/.github/workflows/pr-review-comment.yaml b/.github/workflows/pr-review-comment.yaml index 11b590809a..261ce56177 100644 --- a/.github/workflows/pr-review-comment.yaml +++ b/.github/workflows/pr-review-comment.yaml @@ -35,7 +35,7 @@ jobs: const prNumber = artifact.name.slice('pr_number-'.length) core.setOutput('pr-number', prNumber); - - uses: backstage/actions/re-review@v0.5.13 + - uses: backstage/actions/re-review@v0.6.0 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index acbd11aece..1cde52bfbb 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -18,7 +18,7 @@ jobs: if: github.repository == 'backstage/backstage' && ( github.event.pull_request || github.event.issue.pull_request ) steps: - name: PR sync - uses: backstage/actions/pr-sync@v0.5.13 + uses: backstage/actions/pr-sync@v0.6.0 with: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index 19cc5f8045..b84fe5c2e6 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -20,7 +20,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 9dcc9f63ef..dfd114c66a 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -23,7 +23,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml index a23216a9cb..968bc859d6 100644 --- a/.github/workflows/uffizzi-build.yml +++ b/.github/workflows/uffizzi-build.yml @@ -27,7 +27,7 @@ jobs: registry-url: https://registry.npmjs.org/ - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: linux-v16 diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index f687e4f5a4..16ddb04086 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -52,7 +52,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_kubernetes.yml b/.github/workflows/verify_kubernetes.yml index 7fbcf9fa3f..6d8a7682b5 100644 --- a/.github/workflows/verify_kubernetes.yml +++ b/.github/workflows/verify_kubernetes.yml @@ -28,7 +28,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 0e63393af8..38b685c539 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -35,7 +35,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.0 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: storybook yarn install From 77c41b69241da6c266c9f51e67c755ebe45df756 Mon Sep 17 00:00:00 2001 From: Damon Kaswell Date: Tue, 31 Jan 2023 11:13:06 -0800 Subject: [PATCH 07/90] Added changeset Signed-off-by: Damon Kaswell --- .changeset/tall-tables-hide.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tall-tables-hide.md diff --git a/.changeset/tall-tables-hide.md b/.changeset/tall-tables-hide.md new file mode 100644 index 0000000000..3e8011aff2 --- /dev/null +++ b/.changeset/tall-tables-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +--- + +Updated README to include newer API options for incremental entity providers From 060713cab7b1a750ad92f3ccffe305c49b9cbd67 Mon Sep 17 00:00:00 2001 From: Callum Linington Date: Thu, 2 Feb 2023 10:39:59 +0000 Subject: [PATCH 08/90] docs(azure): add note around token usage Signed-off-by: Callum Linington --- docs/integrations/azure/locations.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md index 3b3e3dd5ed..93a7264af9 100644 --- a/docs/integrations/azure/locations.md +++ b/docs/integrations/azure/locations.md @@ -28,3 +28,5 @@ The configuration is a structure with two elements: - `host`: The DevOps host; only `dev.azure.com` is supported. - `token` (optional): A personal access token as expected by Azure DevOps. + +> Note: The token should just be provided as the raw token generated by Azure DevOps using the format `:{raw_token}` \ No newline at end of file From bb74ee421a629248995b1a78692ed8fb3d6b8a8f Mon Sep 17 00:00:00 2001 From: Callum Linington Date: Thu, 2 Feb 2023 15:19:13 +0000 Subject: [PATCH 09/90] docs(azure): update token usage to align to implementation Signed-off-by: Callum Linington --- docs/integrations/azure/locations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md index 93a7264af9..a0d933d824 100644 --- a/docs/integrations/azure/locations.md +++ b/docs/integrations/azure/locations.md @@ -29,4 +29,4 @@ The configuration is a structure with two elements: - `host`: The DevOps host; only `dev.azure.com` is supported. - `token` (optional): A personal access token as expected by Azure DevOps. -> Note: The token should just be provided as the raw token generated by Azure DevOps using the format `:{raw_token}` \ No newline at end of file +> Note: The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API \ No newline at end of file From df8782378d9a00887713cee932f9f71d50c87c84 Mon Sep 17 00:00:00 2001 From: Callum Linington Date: Thu, 2 Feb 2023 15:44:32 +0000 Subject: [PATCH 10/90] chore: prettier Signed-off-by: Callum Linington --- docs/integrations/azure/locations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md index a0d933d824..1fc476f418 100644 --- a/docs/integrations/azure/locations.md +++ b/docs/integrations/azure/locations.md @@ -29,4 +29,4 @@ The configuration is a structure with two elements: - `host`: The DevOps host; only `dev.azure.com` is supported. - `token` (optional): A personal access token as expected by Azure DevOps. -> Note: The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API \ No newline at end of file +> Note: The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API From c578ab658393caf2c0f9bdaf686efd036925b9b1 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Fri, 3 Feb 2023 10:34:35 +0000 Subject: [PATCH 11/90] docs: adjust code block type in permission tutorial step 1 Signed-off-by: MT Lewis --- docs/permissions/plugin-authors/01-setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/permissions/plugin-authors/01-setup.md b/docs/permissions/plugin-authors/01-setup.md index 04e31b859e..a38b81e679 100644 --- a/docs/permissions/plugin-authors/01-setup.md +++ b/docs/permissions/plugin-authors/01-setup.md @@ -48,7 +48,7 @@ The source code is available here: Create a new `packages/backend/src/plugins/todolist.ts` with the following content: - ```javascript + ```typescript import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import { createRouter } from '@internal/plugin-todo-list-backend'; import { Router } from 'express'; From 8d6d1fb789ba3a6a8982df2083c017dd6583a8ec Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Fri, 3 Feb 2023 10:34:53 +0000 Subject: [PATCH 12/90] docs: miscellaneous permission tutorial step 2 fixes Signed-off-by: MT Lewis --- .../02-adding-a-basic-permission-check.md | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 241b175987..551e48f91a 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -146,7 +146,7 @@ In order to test the logic above, the integrators of your backstage instance nee - async handle(): Promise { + async handle( + request: PolicyQuery, -+ user?: BackstageIdentityResponse, ++ _user?: BackstageIdentityResponse, + ): Promise { + if (isPermission(request.permission, todoListCreatePermission)) { + return { @@ -204,7 +204,7 @@ First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`: const router = await createRouter({ logger: getVoidLogger(), identity: {} as DefaultIdentityClient, -+ permissions: toPermissionEvaluator, ++ permissions: permissionEvaluator, }); app = express().use(router); }); @@ -235,7 +235,7 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ + ServerTokenManager, } from '@backstage/backend-common'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; - import { ServerPermissionClient } from '@backstage/plugin-permission-node'; ++ import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { Server } from 'http'; import { Logger } from 'winston'; import { createRouter } from './router'; @@ -285,4 +285,44 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ module.hot?.accept(); ``` +Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`: + +```diff + import { loggerToWinstonLogger } from '@backstage/backend-common'; + import { + coreServices, + createBackendPlugin, + } from '@backstage/backend-plugin-api'; + import { createRouter } from './service/router'; + + /** + * The example TODO list backend plugin. + * + * @alpha + */ + export const exampleTodoListPlugin = createBackendPlugin({ + id: 'exampleTodoList', + register(env) { + env.registerInit({ + deps: { + identity: coreServices.identity, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, ++ permissions: coreServices.permissions, + }, +- async init({ identity, logger, httpRouter }) { ++ async init({ identity, logger, httpRouter, permissions }) { + httpRouter.use( + await createRouter({ + identity, + logger: loggerToWinstonLogger(logger), + permissions, + }), + ); + }, + }); + }, + }); +``` + Now when you run `yarn tsc` you should have no more errors. From ce2c9ac363abbfcfcbb74eb8436b27c402e49766 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Fri, 3 Feb 2023 10:45:31 +0000 Subject: [PATCH 13/90] docs: miscellaneous permission tutorial step 3 fixes Signed-off-by: MT Lewis --- .../03-adding-a-resource-permission-check.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md index 8913b6d438..b063727c71 100644 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md @@ -76,7 +76,7 @@ This enables decisions based on characteristics of the resource, but it's import Install the missing module: ``` -$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node +$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node zod ``` Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append the following code: @@ -89,7 +89,8 @@ import { Todo, TodoFilter } from './todos'; export const createTodoListPermissionRule = makeCreatePermissionRule< Todo, - TodoFilter + TodoFilter, + typeof TODO_LIST_RESOURCE_TYPE >(); export const isOwner = createTodoListPermissionRule({ @@ -97,8 +98,8 @@ export const isOwner = createTodoListPermissionRule({ description: 'Should allow only if the todo belongs to the user', resourceType: TODO_LIST_RESOURCE_TYPE, paramsSchema: z.object({ - userId: z.string().describe('User ID to match on the resource') - }) + userId: z.string().describe('User ID to match on the resource'), + }), apply: (resource: Todo, { userId }) => { return resource.author === userId; }, @@ -187,6 +188,7 @@ Make sure `todoListConditions` and `createTodoListConditionalDecision` are expor ```diff export * from './service/router'; + export * from './conditionExports'; + export { exampleTodoListPlugin } from './plugin'; ``` ## Test the authorized update endpoint @@ -209,7 +211,6 @@ Let's go back to the permission policy's handle function and try to authorize ou + import { + todoListCreatePermission, + todoListUpdatePermission, -+ TODO_LIST_RESOURCE_TYPE, + } from '@internal/plugin-todo-list-common'; + import { + todoListConditions, @@ -217,7 +218,11 @@ Let's go back to the permission policy's handle function and try to authorize ou + } from '@internal/plugin-todo-list-backend'; ... - + async handle( + request: PolicyQuery, +- _user?: BackstageIdentityResponse, ++ user?: BackstageIdentityResponse, + ): Promise { if (isPermission(request.permission, todoListCreatePermission)) { return { result: AuthorizeResult.ALLOW, @@ -236,6 +241,7 @@ Let's go back to the permission policy's handle function and try to authorize ou return { result: AuthorizeResult.ALLOW, }; + } ``` For any incoming update requests, we now return a _Conditional Decision_. We are saying: From f3bf8fea2439e714734b186c6eeb3db7157f11af Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Fri, 3 Feb 2023 11:00:56 +0000 Subject: [PATCH 14/90] docs: miscellaneous permission tutorial step 4 fixes Signed-off-by: MT Lewis --- .../04-authorizing-access-to-paginated-data.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md index 8ddcffdcd9..e84f53f176 100644 --- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md +++ b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md @@ -15,7 +15,7 @@ One possible solution may leverage the batching functionality to authorize all o - res.json(getAll()) + const items = getAll(); + const decisions = await permissions.authorize( -+ items.map(({ id }) => ({ permission: todosListRead, resourceRef: id })), ++ items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })), + ); + const filteredItems = decisions.filter( @@ -53,7 +53,7 @@ Let's add another permission to the plugin. resourceType: TODO_LIST_RESOURCE_TYPE, }); + -+ export const todosListRead = createPermission({ ++ export const todoListReadPermission = createPermission({ + name: 'todos.list.read', + attributes: { action: 'read' }, + resourceType: TODO_LIST_RESOURCE_TYPE, @@ -81,10 +81,9 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev import { todosListCreate, todosListUpdate, -+ todosListRead, ++ todoListReadPermission, TODO_LIST_RESOURCE_TYPE, } from './permissions'; -+ import { rules } from './rules'; + const transformConditions: ConditionTransformer = createConditionTransformer(Object.values(rules)); @@ -95,7 +94,7 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev + ); + + const decision = ( -+ await permissions.authorizeConditional([{ permission: todosListRead }], { ++ await permissions.authorizeConditional([{ permission: todoListReadPermission }], { + token, + }) + )[0]; @@ -110,7 +109,6 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev + } else { + res.json(getAll()); + } -+ } - res.json(getAll()); }); ``` @@ -121,7 +119,7 @@ Since `TodoFilter` used in our plugin matches the structure of the conditions ob ## Test the authorized read endpoint -Let's update our permission policy to return a conditional result whenever a `todosListRead` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission. +Let's update our permission policy to return a conditional result whenever a `todoListReadPermission` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission. ```diff // packages/backend/src/plugins/permission.ts @@ -132,7 +130,6 @@ import { todoListCreatePermission, todoListUpdatePermission, + todoListReadPermission, - TODO_LIST_RESOURCE_TYPE, } from '@internal/plugin-todo-list-common'; ... From 812ffcc292798cf5202e7f2d6afbcb5586528682 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Fri, 3 Feb 2023 12:55:50 +0000 Subject: [PATCH 15/90] docs: miscellaneous permission tutorial step 5 fixes Signed-off-by: MT Lewis --- .../05-frontend-authorization.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index d494ea60ba..753ce56461 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -56,7 +56,11 @@ Let's make the following changes in `plugins/todo-list/src/components/TodoListPa - Add - + {!loadingPermission && ( -+ + )} @@ -118,7 +122,10 @@ Providing a disabled state can be a helpful signal to users, but there may be ca - - - -+ }> ++ } ++ > + + + @@ -149,11 +156,15 @@ Providing a disabled state can be a helpful signal to users, but there may be ca onChange={e => (title.current = e.target.value)} /> - {!loadingPermission && ( -- - )} -+ From 27226d88a992b0c88dc7fe1767f931d03178f738 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Feb 2023 14:21:57 +0100 Subject: [PATCH 16/90] chore: fixing api-reports Signed-off-by: blam --- plugins/badges-backend/api-report.md | 2 +- plugins/bitbucket-cloud-common/api-report.md | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md index 0ecf41ee85..8ca4b561a2 100644 --- a/plugins/badges-backend/api-report.md +++ b/plugins/badges-backend/api-report.md @@ -79,7 +79,7 @@ export type BadgeSpec = { }; // @public (undocumented) -export type BadgeStyle = typeof BADGE_STYLES[number]; +export type BadgeStyle = (typeof BADGE_STYLES)[number]; // @public (undocumented) export const createDefaultBadgeFactories: () => BadgeFactories; diff --git a/plugins/bitbucket-cloud-common/api-report.md b/plugins/bitbucket-cloud-common/api-report.md index 5182579a4b..95e6f36836 100644 --- a/plugins/bitbucket-cloud-common/api-report.md +++ b/plugins/bitbucket-cloud-common/api-report.md @@ -132,7 +132,7 @@ export namespace Models { readonly Plaintext: 'plaintext'; }; export type BaseCommitSummaryMarkupEnum = - typeof BaseCommitSummaryMarkupEnum[keyof typeof BaseCommitSummaryMarkupEnum]; + (typeof BaseCommitSummaryMarkupEnum)[keyof typeof BaseCommitSummaryMarkupEnum]; export interface Branch { default_merge_strategy?: string; // (undocumented) @@ -150,7 +150,7 @@ export namespace Models { readonly FastForward: 'fast_forward'; }; export type BranchMergeStrategiesEnum = - typeof BranchMergeStrategiesEnum[keyof typeof BranchMergeStrategiesEnum]; + (typeof BranchMergeStrategiesEnum)[keyof typeof BranchMergeStrategiesEnum]; export interface Commit extends BaseCommit { // (undocumented) participants?: Array; @@ -179,7 +179,7 @@ export namespace Models { }; // (undocumented) export type CommitFileAttributesEnum = - typeof CommitFileAttributesEnum[keyof typeof CommitFileAttributesEnum]; + (typeof CommitFileAttributesEnum)[keyof typeof CommitFileAttributesEnum]; export interface Link { // (undocumented) href?: string; @@ -221,7 +221,7 @@ export namespace Models { }; // (undocumented) export type ParticipantRoleEnum = - typeof ParticipantRoleEnum[keyof typeof ParticipantRoleEnum]; + (typeof ParticipantRoleEnum)[keyof typeof ParticipantRoleEnum]; const // (undocumented) ParticipantStateEnum: { readonly Approved: 'approved'; @@ -230,7 +230,7 @@ export namespace Models { }; // (undocumented) export type ParticipantStateEnum = - typeof ParticipantStateEnum[keyof typeof ParticipantStateEnum]; + (typeof ParticipantStateEnum)[keyof typeof ParticipantStateEnum]; export interface Project extends ModelObject { // (undocumented) created_on?: string; @@ -306,7 +306,7 @@ export namespace Models { readonly NoForks: 'no_forks'; }; export type RepositoryForkPolicyEnum = - typeof RepositoryForkPolicyEnum[keyof typeof RepositoryForkPolicyEnum]; + (typeof RepositoryForkPolicyEnum)[keyof typeof RepositoryForkPolicyEnum]; const // (undocumented) RepositoryScmEnum: { readonly Git: 'git'; @@ -336,7 +336,7 @@ export namespace Models { } // (undocumented) export type RepositoryScmEnum = - typeof RepositoryScmEnum[keyof typeof RepositoryScmEnum]; + (typeof RepositoryScmEnum)[keyof typeof RepositoryScmEnum]; // (undocumented) export interface SearchCodeSearchResult { // (undocumented) From 179d3015184ce0b0a2d9b9b1abc7a4f422256943 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Jan 2023 22:55:41 +0100 Subject: [PATCH 17/90] added eslint-plugin Signed-off-by: Patrik Oldsberg --- .changeset/tough-jeans-camp.md | 5 + .github/vale/Vocab/Backstage/accept.txt | 3 +- packages/eslint-plugin/README.md | 40 ++++++ packages/eslint-plugin/index.js | 29 ++++ packages/eslint-plugin/package.json | 16 +++ .../rules/no-forbidden-package-imports.js | 124 ++++++++++++++++++ yarn.lock | 6 + 7 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 .changeset/tough-jeans-camp.md create mode 100644 packages/eslint-plugin/README.md create mode 100644 packages/eslint-plugin/index.js create mode 100644 packages/eslint-plugin/package.json create mode 100644 packages/eslint-plugin/rules/no-forbidden-package-imports.js diff --git a/.changeset/tough-jeans-camp.md b/.changeset/tough-jeans-camp.md new file mode 100644 index 0000000000..b7c08593e1 --- /dev/null +++ b/.changeset/tough-jeans-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/eslint-plugin': minor +--- + +Added a new ESLint plugin with common rules for Backstage projects. diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index f24b701d36..9e7cf7aa98 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -336,6 +336,7 @@ subfolders subheader subheaders subkey +subpath subroutes subtree superfences @@ -418,4 +419,4 @@ Dominik Henneke Kuang Frontside -Kaswell \ No newline at end of file +Kaswell diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md new file mode 100644 index 0000000000..a063d801d3 --- /dev/null +++ b/packages/eslint-plugin/README.md @@ -0,0 +1,40 @@ +# @backstage/eslint-plugin + +A collection of ESLint rules useful to Backstage projects. + +## Usage + +This ESLint plugin is part of the default lint configuration provided by the [Backstage CLI](https://www.npmjs.com/package/@backstage/cli), so you generally do not need to install it manually. + +If you do wish to install this plugin manually, start by adding it as a development dependency to your project: + +```sh +yarn add --dev @backstage/eslint-plugin +``` + +Then add it to your ESLint configuration: + +```js +extends: [ + 'plugin:@backstage/recommended', +], +``` + +Alternatively, if you want to install in individual rules manually: + +```js +plugins: [ + '@backstage', +], +rules: { + '@backstage/no-forbidden-package-imports': 'error', +} +``` + +## Rules + +The following rules are provided by this plugin: + +| Rule | Description | +| ----------------------------------------- | ------------------------------------------------------------------------------- | +| `@backstage/no-forbidden-package-imports` | Disallow internal monorepo imports from package subpaths that are not exported. | diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js new file mode 100644 index 0000000000..11d17b2b8f --- /dev/null +++ b/packages/eslint-plugin/index.js @@ -0,0 +1,29 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + configs: { + recommended: { + plugins: ['@backstage'], + rules: { + '@backstage/no-forbidden-package-imports': 'error', + }, + }, + }, + rules: { + 'no-forbidden-package-imports': require('./rules/no-forbidden-package-imports'), + }, +}; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json new file mode 100644 index 0000000000..9724cec32b --- /dev/null +++ b/packages/eslint-plugin/package.json @@ -0,0 +1,16 @@ +{ + "name": "@backstage/eslint-plugin", + "description": "Backstage ESLint plugin", + "version": "0.0.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/eslint-plugin" + }, + "license": "Apache-2.0", + "main": "./index.js" +} diff --git a/packages/eslint-plugin/rules/no-forbidden-package-imports.js b/packages/eslint-plugin/rules/no-forbidden-package-imports.js new file mode 100644 index 0000000000..d5cce80368 --- /dev/null +++ b/packages/eslint-plugin/rules/no-forbidden-package-imports.js @@ -0,0 +1,124 @@ +'use strict'; +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +// Note: this file is ES5, because we want to avoid having to transpile it for local development + +var fs = require('fs'); +var path = require('path'); +/** @type {import('@manypkg/get-packages')} */ +var manypkg = require('@manypkg/get-packages'); + +// Loads all packages in the monorepo once, and caches the result +var getPackageMap = (function () { + var map = undefined; + var lastLoadAt = undefined; + + /** @returns {Record} */ + return function (/** @type {string} */ dir) { + if (map) { + // Only cache for 5 seconds, to avoid the need to reload ESLint servers + if (Date.now() - lastLoadAt > 5000) { + map = undefined; + } else { + return map; + } + } + var packages = manypkg.getPackagesSync(dir).packages; + if (!packages) { + return undefined; + } + map = {}; + lastLoadAt = Date.now(); + packages.forEach(function (pkg) { + map[pkg.packageJson.name] = pkg; + }); + return map; + }; +})(); + +module.exports = { + meta: { + type: 'problem', + messages: { + forbidden: '{{packageName}} does not export {{subPath}}', + }, + }, + create: function (/** @type import('eslint').Rule.RuleContext */ context) { + var packageMap = getPackageMap(context.getCwd()); + if (!packageMap) { + return; + } + + function checkImport(node) { + if (!node.source) { + return; + } + var importPath = node.source.value; + if (importPath[0] === '.') { + return; + } + + var pathParts = importPath.split('/'); + + // Check for match with plain name, then namespaced name + var name = pathParts.shift(); + if (!packageMap[name]) { + name += '/' + pathParts.shift(); + } + if (!packageMap[name]) { + return; + } + + // Empty subpaths are always allowed + var subPath = pathParts.join('/'); + if (!subPath) { + return; + } + + var pkg = packageMap[name]; + + // If the import is listed in the package.json exports field, we allow it + var exp = pkg.packageJson.exports; + if (exp && (exp[subPath] || exp['./' + subPath])) { + return; + } + + // If there's a package.json in the target directory, we allow the import + if (fs.existsSync(path.resolve(pkg.dir, subPath, 'package.json'))) { + return; + } + + context.report({ + node: node, + messageId: 'forbidden', + data: { + packageName: pkg.packageJson.name || '', + subPath: subPath, + }, + }); + } + + return { + ImportDeclaration: checkImport, + ExportAllDeclaration: checkImport, + ExportNamedDeclaration: checkImport, + ImportExpression: checkImport, + }; + }, +}; diff --git a/yarn.lock b/yarn.lock index 3d809a40c8..ac5c6d0541 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4095,6 +4095,12 @@ __metadata: languageName: unknown linkType: soft +"@backstage/eslint-plugin@workspace:packages/eslint-plugin": + version: 0.0.0-use.local + resolution: "@backstage/eslint-plugin@workspace:packages/eslint-plugin" + languageName: unknown + linkType: soft + "@backstage/integration-aws-node@workspace:^, @backstage/integration-aws-node@workspace:packages/integration-aws-node": version: 0.0.0-use.local resolution: "@backstage/integration-aws-node@workspace:packages/integration-aws-node" From 98b2bd1b51688ae5947a755dc13e023a6fa5e739 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Jan 2023 21:53:55 +0100 Subject: [PATCH 18/90] eslint-plugin: added missing deps + lint config Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/.eslintrc.js | 35 +++++++++++++++++++++++++++++ packages/eslint-plugin/package.json | 11 ++++++++- yarn.lock | 3 +++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/eslint-plugin/.eslintrc.js diff --git a/packages/eslint-plugin/.eslintrc.js b/packages/eslint-plugin/.eslintrc.js new file mode 100644 index 0000000000..aaf907cfbc --- /dev/null +++ b/packages/eslint-plugin/.eslintrc.js @@ -0,0 +1,35 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Custom lint config here since source is ES5 +module.exports = { + plugins: ['import'], + parserOptions: { + ecmaVersion: 5, + }, + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: false, + optionalDependencies: true, + peerDependencies: true, + bundledDependencies: true, + }, + ], + 'no-unused-expressions': 'off', + }, +}; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 9724cec32b..ee5e62c41e 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -12,5 +12,14 @@ "directory": "packages/eslint-plugin" }, "license": "Apache-2.0", - "main": "./index.js" + "main": "./index.js", + "scripts": { + "lint": "backstage-cli package lint" + }, + "dependencies": { + "@manypkg/get-packages": "^1.1.3" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } } diff --git a/yarn.lock b/yarn.lock index ac5c6d0541..06cd22d84a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4098,6 +4098,9 @@ __metadata: "@backstage/eslint-plugin@workspace:packages/eslint-plugin": version: 0.0.0-use.local resolution: "@backstage/eslint-plugin@workspace:packages/eslint-plugin" + dependencies: + "@backstage/cli": "workspace:^" + "@manypkg/get-packages": ^1.1.3 languageName: unknown linkType: soft From 4d05d5bf8362ac5e86c6e75918aa7cdbb14a3a79 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 11:10:38 +0100 Subject: [PATCH 19/90] eslint-plugin: refactor to ES2021 and add no-undeclared-imports rule Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/.eslintrc.js | 7 +- packages/eslint-plugin/index.js | 2 + packages/eslint-plugin/lib/getPackages.js | 63 +++++++ packages/eslint-plugin/lib/visitImports.js | 100 +++++++++++ packages/eslint-plugin/package.json | 3 +- .../rules/no-forbidden-package-imports.js | 91 ++-------- .../rules/no-undeclared-imports.js | 156 ++++++++++++++++++ yarn.lock | 1 + 8 files changed, 339 insertions(+), 84 deletions(-) create mode 100644 packages/eslint-plugin/lib/getPackages.js create mode 100644 packages/eslint-plugin/lib/visitImports.js create mode 100644 packages/eslint-plugin/rules/no-undeclared-imports.js diff --git a/packages/eslint-plugin/.eslintrc.js b/packages/eslint-plugin/.eslintrc.js index aaf907cfbc..74f93a6475 100644 --- a/packages/eslint-plugin/.eslintrc.js +++ b/packages/eslint-plugin/.eslintrc.js @@ -17,8 +17,9 @@ // Custom lint config here since source is ES5 module.exports = { plugins: ['import'], - parserOptions: { - ecmaVersion: 5, + env: { + node: true, + es2021: true, }, rules: { 'import/no-extraneous-dependencies': [ @@ -30,6 +31,6 @@ module.exports = { bundledDependencies: true, }, ], - 'no-unused-expressions': 'off', + // 'no-unused-expressions': 'off', }, }; diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js index 11d17b2b8f..cf1dd20ab7 100644 --- a/packages/eslint-plugin/index.js +++ b/packages/eslint-plugin/index.js @@ -20,10 +20,12 @@ module.exports = { plugins: ['@backstage'], rules: { '@backstage/no-forbidden-package-imports': 'error', + '@backstage/no-undeclared-imports': 'error', }, }, }, rules: { 'no-forbidden-package-imports': require('./rules/no-forbidden-package-imports'), + 'no-undeclared-imports': require('./rules/no-undeclared-imports'), }, }; diff --git a/packages/eslint-plugin/lib/getPackages.js b/packages/eslint-plugin/lib/getPackages.js new file mode 100644 index 0000000000..91043fe0d5 --- /dev/null +++ b/packages/eslint-plugin/lib/getPackages.js @@ -0,0 +1,63 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** @type {import('@manypkg/get-packages')} */ +const manypkg = require('@manypkg/get-packages'); + +/** + * @typedef ExtendedPackage + * @type {import('@manypkg/get-packages').Package & { packageJson: { exports?: Record }}} packageJson + */ + +/** + * @typedef PackageMap + * @type object + * + * @property {ExtendedPackage} root + * @property {ExtendedPackage[]} list + * @property {Map} map + */ + +// Loads all packages in the monorepo once, and caches the result +module.exports = (function () { + let result = undefined; + let lastLoadAt = undefined; + + /** @returns {PackageMap | undefined} */ + return function getPackages(/** @type {string} */ dir) { + if (result) { + // Only cache for 5 seconds, to avoid the need to reload ESLint servers + if (Date.now() - lastLoadAt > 5000) { + result = undefined; + } else { + return result; + } + } + const packages = manypkg.getPackagesSync(dir); + if (!packages) { + return undefined; + } + result = { + map: new Map(packages.packages.map(pkg => [pkg.packageJson.name, pkg])), + list: packages.packages, + root: packages.root, + }; + lastLoadAt = Date.now(); + return result; + }; +})(); diff --git a/packages/eslint-plugin/lib/visitImports.js b/packages/eslint-plugin/lib/visitImports.js new file mode 100644 index 0000000000..d73c3203a8 --- /dev/null +++ b/packages/eslint-plugin/lib/visitImports.js @@ -0,0 +1,100 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const getPackages = require('./getPackages'); + +/** + * @typedef LocalImport + * @type {object} + * @property {'local'} type + * @property {string} path + */ + +/** + * @typedef InternalImport + * @type {object} + * @property {'internal'} type + * @property {string} path + * @property {import('./getPackages').ExtendedPackage} package + */ + +/** + * @typedef ExternalImport + * @type {object} + * @property {'external'} type + * @property {string} path + * @property {string} packageName + */ + +/** + * @callback ImportVisitor + * @param {import('eslint').Rule.Node} node + * @param {LocalImport | InternalImport | ExternalImport} import + */ + +/** + * @param visitor - Visitor callback + * @param {import('eslint').Rule.RuleContext} context + * @param {ImportVisitor} visitor + */ +module.exports = function visitImports(context, visitor) { + const packages = getPackages(context.getCwd()); + if (!packages) { + return; + } + + function visit(node) { + if (!node.source) { + return; + } + const importPath = node.source.value; + if (importPath[0] === '.') { + return visitor(node, { type: 'local', path: importPath }); + } + + const pathParts = importPath.split('/'); + + // Check for match with plain name, then namespaced name + let packageName; + let subPath; + if (importPath[0] === '@') { + packageName = pathParts.slice(0, 2).join('/'); + subPath = pathParts.slice(2).join('/'); + } else { + packageName = pathParts[0]; + subPath = pathParts.slice(1).join('/'); + } + const pkg = packages?.map.get(packageName); + if (!pkg) { + return visitor(node, { + type: 'external', + path: subPath, + packageName: packageName, + }); + } + + return visitor(node, { type: 'internal', path: subPath, package: pkg }); + } + + return { + ImportDeclaration: visit, + ExportAllDeclaration: visit, + ExportNamedDeclaration: visit, + ImportExpression: visit, + }; +}; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index ee5e62c41e..73862d2295 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -17,7 +17,8 @@ "lint": "backstage-cli package lint" }, "dependencies": { - "@manypkg/get-packages": "^1.1.3" + "@manypkg/get-packages": "^1.1.3", + "minimatch": "^5.1.2" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/packages/eslint-plugin/rules/no-forbidden-package-imports.js b/packages/eslint-plugin/rules/no-forbidden-package-imports.js index d5cce80368..bbf342dba0 100644 --- a/packages/eslint-plugin/rules/no-forbidden-package-imports.js +++ b/packages/eslint-plugin/rules/no-forbidden-package-imports.js @@ -1,4 +1,3 @@ -'use strict'; /* * Copyright 2023 The Backstage Authors * @@ -17,41 +16,9 @@ // @ts-check -// Note: this file is ES5, because we want to avoid having to transpile it for local development - -var fs = require('fs'); -var path = require('path'); -/** @type {import('@manypkg/get-packages')} */ -var manypkg = require('@manypkg/get-packages'); - -// Loads all packages in the monorepo once, and caches the result -var getPackageMap = (function () { - var map = undefined; - var lastLoadAt = undefined; - - /** @returns {Record} */ - return function (/** @type {string} */ dir) { - if (map) { - // Only cache for 5 seconds, to avoid the need to reload ESLint servers - if (Date.now() - lastLoadAt > 5000) { - map = undefined; - } else { - return map; - } - } - var packages = manypkg.getPackagesSync(dir).packages; - if (!packages) { - return undefined; - } - map = {}; - lastLoadAt = Date.now(); - packages.forEach(function (pkg) { - map[pkg.packageJson.name] = pkg; - }); - return map; - }; -})(); +const visitImports = require('../lib/visitImports'); +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'problem', @@ -59,48 +26,19 @@ module.exports = { forbidden: '{{packageName}} does not export {{subPath}}', }, }, - create: function (/** @type import('eslint').Rule.RuleContext */ context) { - var packageMap = getPackageMap(context.getCwd()); - if (!packageMap) { - return; - } - - function checkImport(node) { - if (!node.source) { + create(context) { + return visitImports(context, (node, imp) => { + if (imp.type !== 'internal') { return; } - var importPath = node.source.value; - if (importPath[0] === '.') { - return; - } - - var pathParts = importPath.split('/'); - - // Check for match with plain name, then namespaced name - var name = pathParts.shift(); - if (!packageMap[name]) { - name += '/' + pathParts.shift(); - } - if (!packageMap[name]) { - return; - } - // Empty subpaths are always allowed - var subPath = pathParts.join('/'); - if (!subPath) { + if (!imp.path) { return; } - var pkg = packageMap[name]; - // If the import is listed in the package.json exports field, we allow it - var exp = pkg.packageJson.exports; - if (exp && (exp[subPath] || exp['./' + subPath])) { - return; - } - - // If there's a package.json in the target directory, we allow the import - if (fs.existsSync(path.resolve(pkg.dir, subPath, 'package.json'))) { + const exp = imp.package.packageJson.exports; + if (exp && (exp[imp.path] || exp['./' + imp.path])) { return; } @@ -108,17 +46,10 @@ module.exports = { node: node, messageId: 'forbidden', data: { - packageName: pkg.packageJson.name || '', - subPath: subPath, + packageName: imp.package.packageJson.name || imp.package.dir, + subPath: imp.path, }, }); - } - - return { - ImportDeclaration: checkImport, - ExportAllDeclaration: checkImport, - ExportNamedDeclaration: checkImport, - ImportExpression: checkImport, - }; + }); }, }; diff --git a/packages/eslint-plugin/rules/no-undeclared-imports.js b/packages/eslint-plugin/rules/no-undeclared-imports.js new file mode 100644 index 0000000000..462b4b2e31 --- /dev/null +++ b/packages/eslint-plugin/rules/no-undeclared-imports.js @@ -0,0 +1,156 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const path = require('path'); +const getPackageMap = require('../lib/getPackages'); +const visitImports = require('../lib/visitImports'); +const minimatch = require('minimatch'); + +const depFields = { + dep: 'dependencies', + dev: 'devDependencies', + peer: 'peerDependencies', +}; + +const peerDeps = ['react', 'react-dom', 'react-router', 'react-router-dom']; +const devModulePatterns = [ + new minimatch.Minimatch('!src/**'), + new minimatch.Minimatch('src/**/*.test.*'), + new minimatch.Minimatch('src/**/*.stories.*'), + new minimatch.Minimatch('src/**/__testUtils__/**'), + new minimatch.Minimatch('src/**/__mocks__/**'), + new minimatch.Minimatch('src/setupTests.*'), +]; + +function getExpectedDepType( + /** @type {string} */ impPath, + /** @type {string} */ modulePath, +) { + if (peerDeps.includes(impPath)) { + return 'peer'; + } + for (const pattern of devModulePatterns) { + if (pattern.match(modulePath)) { + return 'dev'; + } + } + return 'dep'; +} + +/** + * + * @param {import('@manypkg/get-packages').Package['packageJson']} pkg + * @param {string} name + * @param {ReturnType} expectedType + * @returns {{oldDepsField?: string, depsField: string} | undefined} + */ +function findConflict(pkg, name, expectedType) { + const isDep = pkg.dependencies?.[name]; + const isDevDep = pkg.devDependencies?.[name]; + const isPeerDep = pkg.peerDependencies?.[name]; + const depsField = depFields[expectedType]; + + if (expectedType === 'dep' && !isDep) { + const oldDepsField = isDevDep ? depFields.dev : undefined; + return { oldDepsField, depsField }; + } else if (expectedType === 'dev' && !isDevDep && !isDep && !isPeerDep) { + return { oldDepsField: undefined, depsField }; + } else if (expectedType === 'peer' && !isPeerDep) { + const oldDepsField = isDep + ? depFields.dep + : isDevDep + ? depFields.dev + : undefined; + + return { oldDepsField, depsField }; + } +} + +function getAddFlagForConflict(conflict) { + switch (conflict?.depsField) { + case depFields.dep: + return ''; + case depFields.dev: + return ' --dev'; + case depFields.peer: + return ' --peer'; + default: + return ''; + } +} + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + messages: { + undeclared: + "{{ packageName }} must be declared in {{ depsField }} of {{ packageJsonPath }}, run 'yarn --cwd {{ packagePath }} add{{ addFlag }} {{ packageName }}' from the project root.", + switch: + '{{ packageName }} is declared in {{ oldDepsField }}, but should be moved to {{ depsField }} in {{ packageJsonPath }}.', + }, + }, + create(context) { + const packages = getPackageMap(context.getCwd()); + if (!packages) { + return {}; + } + const filePath = context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename(); + + const localPkg = packages.list.find(p => + filePath.startsWith(p.dir + path.sep), + ); + if (!localPkg) { + return {}; + } + + return visitImports(context, (node, imp) => { + if (imp.type !== 'external') { + return; + } + + const modulePath = path.relative(localPkg.dir, filePath); + const expectedType = getExpectedDepType(imp.packageName, modulePath); + + const conflict = findConflict( + localPkg.packageJson, + imp.packageName, + expectedType, + ); + + if (conflict) { + const packagePath = path.relative(packages.root.dir, localPkg.dir); + const packageJsonPath = path.join(packagePath, 'package.json'); + + context.report({ + node, + messageId: conflict.oldDepsField ? 'switch' : 'undeclared', + data: { + ...conflict, + packagePath, + addFlag: getAddFlagForConflict(conflict), + packageName: imp.packageName, + packageJsonPath: packageJsonPath, + }, + }); + } + }); + }, +}; diff --git a/yarn.lock b/yarn.lock index 06cd22d84a..e0e1da349c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4101,6 +4101,7 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@manypkg/get-packages": ^1.1.3 + minimatch: ^5.1.2 languageName: unknown linkType: soft From f459b168f03033ca6c4025ce16941d7bc0480835 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 14:19:19 +0100 Subject: [PATCH 20/90] eslint-plugin: add support for detecting Node.js builtins Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/lib/visitImports.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/lib/visitImports.js b/packages/eslint-plugin/lib/visitImports.js index d73c3203a8..c89d65fb49 100644 --- a/packages/eslint-plugin/lib/visitImports.js +++ b/packages/eslint-plugin/lib/visitImports.js @@ -16,6 +16,7 @@ // @ts-check +const { builtinModules } = require('module'); const getPackages = require('./getPackages'); /** @@ -41,10 +42,18 @@ const getPackages = require('./getPackages'); * @property {string} packageName */ +/** + * @typedef BuiltinImport + * @type {object} + * @property {'builtin'} type + * @property {string} path + * @property {string} packageName + */ + /** * @callback ImportVisitor * @param {import('eslint').Rule.Node} node - * @param {LocalImport | InternalImport | ExternalImport} import + * @param {LocalImport | InternalImport | ExternalImport | BuiltinImport} import */ /** @@ -81,10 +90,17 @@ module.exports = function visitImports(context, visitor) { } const pkg = packages?.map.get(packageName); if (!pkg) { + if ( + packageName.startsWith('node:') || + builtinModules.includes(packageName) + ) { + return visitor(node, { type: 'builtin', path: subPath, packageName }); + } + return visitor(node, { type: 'external', path: subPath, - packageName: packageName, + packageName, }); } From 9e192d7f4a67e5a1c1ad35b2b6c27e2207b6c079 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 14:19:41 +0100 Subject: [PATCH 21/90] eslint-plugin: dogfood lint config Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/.eslintrc.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/.eslintrc.js b/packages/eslint-plugin/.eslintrc.js index 74f93a6475..7fc68c3c5d 100644 --- a/packages/eslint-plugin/.eslintrc.js +++ b/packages/eslint-plugin/.eslintrc.js @@ -16,21 +16,13 @@ // Custom lint config here since source is ES5 module.exports = { - plugins: ['import'], + plugins: ['@backstage'], env: { node: true, es2021: true, }, rules: { - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: false, - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], - // 'no-unused-expressions': 'off', + '@backstage/no-undeclared-imports': ['error'], + 'no-unused-expressions': 'off', }, }; From 8b4a758ed28e1d1268894f6d71d0847e365e286f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 14:38:40 +0100 Subject: [PATCH 22/90] eslint-plugin: add support for require calls Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/lib/visitImports.js | 36 ++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/lib/visitImports.js b/packages/eslint-plugin/lib/visitImports.js index c89d65fb49..5184fb8d79 100644 --- a/packages/eslint-plugin/lib/visitImports.js +++ b/packages/eslint-plugin/lib/visitImports.js @@ -56,6 +56,36 @@ const getPackages = require('./getPackages'); * @param {LocalImport | InternalImport | ExternalImport | BuiltinImport} import */ +/** + * @param {import('estree').ImportDeclaration | import('estree').ExportAllDeclaration | import('estree').ExportNamedDeclaration | import('estree').ImportExpression | import('estree').SimpleCallExpression} node + * @returns {string | undefined} + */ +function getImportPath(node) { + /** @type {import('estree').Expression | import('estree').SpreadElement | undefined | null} */ + let pathNode; + + if (node.type === 'CallExpression') { + if ( + node.callee.type === 'Identifier' && + node.callee.name == 'require' && + node.arguments.length === 1 + ) { + pathNode = node.arguments[0]; + } + } else { + pathNode = node.source; + } + + if (pathNode?.type !== 'Literal') { + return undefined; + } + if (typeof pathNode.value !== 'string') { + return undefined; + } + + return pathNode.value; +} + /** * @param visitor - Visitor callback * @param {import('eslint').Rule.RuleContext} context @@ -68,10 +98,11 @@ module.exports = function visitImports(context, visitor) { } function visit(node) { - if (!node.source) { + const importPath = getImportPath(node); + if (!importPath) { return; } - const importPath = node.source.value; + if (importPath[0] === '.') { return visitor(node, { type: 'local', path: importPath }); } @@ -112,5 +143,6 @@ module.exports = function visitImports(context, visitor) { ExportAllDeclaration: visit, ExportNamedDeclaration: visit, ImportExpression: visit, + CallExpression: visit, }; }; From 765b7ee355ddcbb01c57942796a7cc57952c7839 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 16:14:07 +0100 Subject: [PATCH 23/90] eslint-plugin: backwards compatiblity check for packages without exports Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/lib/getPackages.js | 2 +- .../rules/no-forbidden-package-imports.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/lib/getPackages.js b/packages/eslint-plugin/lib/getPackages.js index 91043fe0d5..318d0654c2 100644 --- a/packages/eslint-plugin/lib/getPackages.js +++ b/packages/eslint-plugin/lib/getPackages.js @@ -21,7 +21,7 @@ const manypkg = require('@manypkg/get-packages'); /** * @typedef ExtendedPackage - * @type {import('@manypkg/get-packages').Package & { packageJson: { exports?: Record }}} packageJson + * @type {import('@manypkg/get-packages').Package & { packageJson: { exports?: Record, files?: Array }}} packageJson */ /** diff --git a/packages/eslint-plugin/rules/no-forbidden-package-imports.js b/packages/eslint-plugin/rules/no-forbidden-package-imports.js index bbf342dba0..6c861e17f5 100644 --- a/packages/eslint-plugin/rules/no-forbidden-package-imports.js +++ b/packages/eslint-plugin/rules/no-forbidden-package-imports.js @@ -41,6 +41,20 @@ module.exports = { if (exp && (exp[imp.path] || exp['./' + imp.path])) { return; } + if (!exp) { + // If there's no exports field, we allow anything listed in files, except dist + const files = imp.package.packageJson.files; + if ( + !files || + files.some(f => !f.startsWith('dist') && imp.path.startsWith(f)) + ) { + return; + } + // And also package.json + if (imp.path === 'package.json') { + return; + } + } context.report({ node: node, From d5eee2944cdc4ccb83f643ad22197c8064e28959 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 16:15:17 +0100 Subject: [PATCH 24/90] eslint-plugin: improved peer deps check and consider package role Signed-off-by: Patrik Oldsberg --- .../rules/no-undeclared-imports.js | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/rules/no-undeclared-imports.js b/packages/eslint-plugin/rules/no-undeclared-imports.js index 462b4b2e31..cec3503f8f 100644 --- a/packages/eslint-plugin/rules/no-undeclared-imports.js +++ b/packages/eslint-plugin/rules/no-undeclared-imports.js @@ -27,7 +27,6 @@ const depFields = { peer: 'peerDependencies', }; -const peerDeps = ['react', 'react-dom', 'react-router', 'react-router-dom']; const devModulePatterns = [ new minimatch.Minimatch('!src/**'), new minimatch.Minimatch('src/**/*.test.*'), @@ -38,12 +37,34 @@ const devModulePatterns = [ ]; function getExpectedDepType( + localPkg, /** @type {string} */ impPath, /** @type {string} */ modulePath, ) { - if (peerDeps.includes(impPath)) { - return 'peer'; + const role = localPkg?.backstage?.role; + // Some package roles have known dependency types + switch (role) { + case 'common-library': + case 'web-library': + case 'frontend-plugin': + case 'frontend-plugin-module': + case 'node-library': + case 'backend-plugin': + case 'backend-plugin-module': + switch (impPath) { + case 'react': + case 'react-dom': + case 'react-router': + case 'react-router-dom': + return 'peer'; + } + case 'cli': + case 'frontend': + case 'backend': + default: + break; } + for (const pattern of devModulePatterns) { if (pattern.match(modulePath)) { return 'dev'; @@ -65,7 +86,7 @@ function findConflict(pkg, name, expectedType) { const isPeerDep = pkg.peerDependencies?.[name]; const depsField = depFields[expectedType]; - if (expectedType === 'dep' && !isDep) { + if (expectedType === 'dep' && !isDep && !isPeerDep) { const oldDepsField = isDevDep ? depFields.dev : undefined; return { oldDepsField, depsField }; } else if (expectedType === 'dev' && !isDevDep && !isDep && !isPeerDep) { @@ -127,7 +148,11 @@ module.exports = { } const modulePath = path.relative(localPkg.dir, filePath); - const expectedType = getExpectedDepType(imp.packageName, modulePath); + const expectedType = getExpectedDepType( + localPkg, + imp.packageName, + modulePath, + ); const conflict = findConflict( localPkg.packageJson, From 58e481a4f9a4889d11720b18727d96eb2406b749 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 16:15:48 +0100 Subject: [PATCH 25/90] eslint-plugin: ignore type imports when checking deps Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/lib/visitImports.js | 39 +++++++++++++------ .../rules/no-undeclared-imports.js | 16 ++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/lib/visitImports.js b/packages/eslint-plugin/lib/visitImports.js index 5184fb8d79..ed16c26c7f 100644 --- a/packages/eslint-plugin/lib/visitImports.js +++ b/packages/eslint-plugin/lib/visitImports.js @@ -23,6 +23,7 @@ const getPackages = require('./getPackages'); * @typedef LocalImport * @type {object} * @property {'local'} type + * @property {'value' | 'type'} kind * @property {string} path */ @@ -30,6 +31,7 @@ const getPackages = require('./getPackages'); * @typedef InternalImport * @type {object} * @property {'internal'} type + * @property {'value' | 'type'} kind * @property {string} path * @property {import('./getPackages').ExtendedPackage} package */ @@ -38,6 +40,7 @@ const getPackages = require('./getPackages'); * @typedef ExternalImport * @type {object} * @property {'external'} type + * @property {'value' | 'type'} kind * @property {string} path * @property {string} packageName */ @@ -46,6 +49,7 @@ const getPackages = require('./getPackages'); * @typedef BuiltinImport * @type {object} * @property {'builtin'} type + * @property {'value' | 'type'} kind * @property {string} path * @property {string} packageName */ @@ -58,9 +62,9 @@ const getPackages = require('./getPackages'); /** * @param {import('estree').ImportDeclaration | import('estree').ExportAllDeclaration | import('estree').ExportNamedDeclaration | import('estree').ImportExpression | import('estree').SimpleCallExpression} node - * @returns {string | undefined} + * @returns {undefined | {path: string, kind: 'type' | 'value'}} */ -function getImportPath(node) { +function getImportInfo(node) { /** @type {import('estree').Expression | import('estree').SpreadElement | undefined | null} */ let pathNode; @@ -83,7 +87,9 @@ function getImportPath(node) { return undefined; } - return pathNode.value; + /** @type {any} */ + const anyNode = node; + return { path: pathNode.value, kind: anyNode.importKind ?? 'value' }; } /** @@ -98,21 +104,21 @@ module.exports = function visitImports(context, visitor) { } function visit(node) { - const importPath = getImportPath(node); - if (!importPath) { + const info = getImportInfo(node); + if (!info) { return; } - if (importPath[0] === '.') { - return visitor(node, { type: 'local', path: importPath }); + if (info.path[0] === '.') { + return visitor(node, { type: 'local', ...info }); } - const pathParts = importPath.split('/'); + const pathParts = info.path.split('/'); // Check for match with plain name, then namespaced name let packageName; let subPath; - if (importPath[0] === '@') { + if (info.path[0] === '@') { packageName = pathParts.slice(0, 2).join('/'); subPath = pathParts.slice(2).join('/'); } else { @@ -125,17 +131,28 @@ module.exports = function visitImports(context, visitor) { packageName.startsWith('node:') || builtinModules.includes(packageName) ) { - return visitor(node, { type: 'builtin', path: subPath, packageName }); + return visitor(node, { + type: 'builtin', + kind: info.kind, + path: subPath, + packageName, + }); } return visitor(node, { type: 'external', + kind: info.kind, path: subPath, packageName, }); } - return visitor(node, { type: 'internal', path: subPath, package: pkg }); + return visitor(node, { + type: 'internal', + kind: info.kind, + path: subPath, + package: pkg, + }); } return { diff --git a/packages/eslint-plugin/rules/no-undeclared-imports.js b/packages/eslint-plugin/rules/no-undeclared-imports.js index cec3503f8f..f7f7b17354 100644 --- a/packages/eslint-plugin/rules/no-undeclared-imports.js +++ b/packages/eslint-plugin/rules/no-undeclared-imports.js @@ -146,6 +146,10 @@ module.exports = { if (imp.type !== 'external') { return; } + // We leave checking of type imports to the repo-tools check + if (imp.kind === 'type') { + return; + } const modulePath = path.relative(localPkg.dir, filePath); const expectedType = getExpectedDepType( @@ -161,6 +165,18 @@ module.exports = { ); if (conflict) { + try { + const fullImport = imp.path + ? `${imp.packageName}/${imp.path}` + : imp.packageName; + require.resolve(fullImport, { + paths: [localPkg.dir], + }); + } catch { + // If the dependency doesn't resolve then it's likely a type import, ignore + return; + } + const packagePath = path.relative(packages.root.dir, localPkg.dir); const packageJsonPath = path.join(packagePath, 'package.json'); From 11df6f42b9c0b8ddb5408b1fb5b5196558f1c413 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 16:16:41 +0100 Subject: [PATCH 26/90] cli: use new eslint plugin + fix issues Signed-off-by: Patrik Oldsberg --- cypress/.eslintrc.js | 9 ------ packages/app/cypress/.eslintrc.json | 9 ------ .../backend-defaults/src/CreateBackend.ts | 2 +- packages/cli/asset-types/asset-types.d.ts | 2 +- packages/cli/config/eslint-factory.js | 31 ++----------------- .../lib/builder/buildTypeDefinitionsWorker.ts | 2 +- packages/cli/src/lib/version.test.ts | 1 - packages/codemods/.eslintrc.js | 15 +-------- packages/codemods/package.json | 2 +- .../LogViewer/RealLogViewer.test.tsx | 2 +- .../LogViewer/useLogViewerSelection.test.tsx | 2 +- packages/create-app/bin/backstage-create-app | 2 +- .../packages/app/cypress/.eslintrc.json | 9 ------ packages/e2e-test/.eslintrc.js | 15 +-------- packages/e2e-test/package.json | 16 +++++----- packages/e2e-test/src/commands/run.ts | 2 +- .../src/commands/type-deps/type-deps.ts | 2 +- .../cypress/.eslintrc.json | 9 ------ packages/techdocs-cli/cypress/.eslintrc.json | 9 ------ .../src/run.ts | 2 +- plugins/code-climate/dev/index.tsx | 2 +- plugins/code-climate/src/setupTests.ts | 2 +- .../example/templates/CostInsightsClient.ts | 2 +- plugins/example-todo-list/.eslintrc.js | 15 +-------- .../scripts/build-nunjucks.js | 2 +- .../src/components/TriggerDialog/testUtils.ts | 2 +- scripts/assemble-manifest.js | 2 +- scripts/check-docs-quality.js | 2 +- scripts/check-if-release.js | 2 +- scripts/create-github-release.js | 2 +- scripts/create-release-tag.js | 2 +- scripts/list-deprecations.js | 2 +- scripts/prepare-release.js | 2 +- scripts/snyk-github-issue-sync.ts | 2 +- scripts/verify-api-reference.js | 2 +- scripts/verify-changesets.js | 2 +- scripts/verify-links.js | 2 +- scripts/verify-lockfile-duplicates.js | 2 +- 38 files changed, 42 insertions(+), 150 deletions(-) diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js index a77de4a629..07f264452d 100644 --- a/cypress/.eslintrc.js +++ b/cypress/.eslintrc.js @@ -2,15 +2,6 @@ module.exports = { extends: [require.resolve('@backstage/cli/config/eslint.backend')], rules: { 'no-console': 0, - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: false, - peerDependencies: false, - bundledDependencies: false, - }, - ], 'jest/valid-expect': 'off', 'jest/expect-expect': 'off', 'no-restricted-syntax': 'off', diff --git a/packages/app/cypress/.eslintrc.json b/packages/app/cypress/.eslintrc.json index 2b3a458b95..a467608916 100644 --- a/packages/app/cypress/.eslintrc.json +++ b/packages/app/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index e6b5a15336..5cbed2c3df 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -40,7 +40,7 @@ import { } from '@backstage/backend-plugin-api'; // Internal import of the type to avoid needing to export this. -// eslint-disable-next-line monorepo/no-internal-import +// eslint-disable-next-line @backstage/no-forbidden-package-imports import type { InternalSharedBackendEnvironment } from '@backstage/backend-plugin-api/src/wiring/createSharedEnvironment'; export const defaultServiceFactories = [ diff --git a/packages/cli/asset-types/asset-types.d.ts b/packages/cli/asset-types/asset-types.d.ts index 0bd0922004..9bd158bb8e 100644 --- a/packages/cli/asset-types/asset-types.d.ts +++ b/packages/cli/asset-types/asset-types.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /// /// diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index a40aa79fb3..f344af4ab0 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -60,11 +60,11 @@ function createConfig(dir, extraConfig = {}) { '@spotify/eslint-config-typescript', 'prettier', 'plugin:jest/recommended', - 'plugin:monorepo/recommended', + 'plugin:@backstage/recommended', ...(extraExtends ?? []), ], parser: '@typescript-eslint/parser', - plugins: ['import', ...(plugins ?? [])], + plugins: ['import', 'monorepo', ...(plugins ?? [])], env: { jest: true, ...env, @@ -87,33 +87,8 @@ function createConfig(dir, extraConfig = {}) { '@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-redeclare': 'error', 'no-undef': 'off', + 'monorepo/no-relative-import': 'error', 'import/newline-after-import': 'error', - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: dir - ? [ - `!${joinPath(dir, 'src/**')}`, - joinPath(dir, 'src/**/*.test.*'), - joinPath(dir, 'src/**/*.stories.*'), - joinPath(dir, 'src/**/__testUtils__/**'), - joinPath(dir, 'src/**/__mocks__/**'), - joinPath(dir, 'src/setupTests.*'), - ] - : [ - // Legacy config for packages that don't provide a dir - '**/*.test.*', - '**/*.stories.*', - '**/__testUtils__/**', - '**/__mocks__/**', - '**/src/setupTests.*', - '**/dev/**', - ], - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', '@typescript-eslint/consistent-type-assertions': 'error', diff --git a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts index 2533131a40..cf37c2a664 100644 --- a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts +++ b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts @@ -50,7 +50,7 @@ export async function buildTypeDefinitionsWorker( */ const { PackageJsonLookup, - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-undeclared-imports } = require('@rushstack/node-core-library/lib/PackageJsonLookup'); const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor; diff --git a/packages/cli/src/lib/version.test.ts b/packages/cli/src/lib/version.test.ts index fb9bf83e7c..604129679b 100644 --- a/packages/cli/src/lib/version.test.ts +++ b/packages/cli/src/lib/version.test.ts @@ -17,7 +17,6 @@ import mockFs from 'mock-fs'; import { packageVersions, createPackageVersionProvider } from './version'; import { Lockfile } from './versioning'; -// eslint-disable-next-line monorepo/no-internal-import import corePluginApiPkg from '@backstage/core-plugin-api/package.json'; describe('createPackageVersionProvider', () => { diff --git a/packages/codemods/.eslintrc.js b/packages/codemods/.eslintrc.js index f9fb2e27c9..e2a53a6ad2 100644 --- a/packages/codemods/.eslintrc.js +++ b/packages/codemods/.eslintrc.js @@ -1,14 +1 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - 'no-console': 0, - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: false, - peerDependencies: false, - bundledDependencies: false, - }, - ], - }, -}); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/codemods/package.json b/packages/codemods/package.json index cbcc0613d4..1ae4d04485 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -35,6 +35,7 @@ "dependencies": { "@backstage/cli-common": "workspace:^", "chalk": "^4.0.0", + "commander": "^9.1.0", "jscodeshift": "^0.14.0", "jscodeshift-add-imports": "^1.0.10" }, @@ -42,7 +43,6 @@ "@backstage/cli": "workspace:^", "@types/jscodeshift": "^0.11.0", "@types/node": "^16.11.26", - "commander": "^9.1.0", "ts-node": "^10.0.0" }, "nodemonConfig": { diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx index dd707778e1..e1aac76381 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx @@ -18,7 +18,7 @@ import React, { ReactNode } from 'react'; import userEvent from '@testing-library/user-event'; import { renderInTestApp } from '@backstage/test-utils'; import { RealLogViewer } from './RealLogViewer'; -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import copyToClipboard from 'copy-to-clipboard'; // Used by useCopyToClipboard diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx index 7e00d50f33..93a6dfd7e9 100644 --- a/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx @@ -20,7 +20,7 @@ import { TestApiProvider, MockErrorApi } from '@backstage/test-utils'; import { errorApiRef } from '@backstage/core-plugin-api'; import { AnsiLine } from './AnsiProcessor'; import { useLogViewerSelection } from './useLogViewerSelection'; -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import copyToClipboard from 'copy-to-clipboard'; // Used by useCopyToClipboard diff --git a/packages/create-app/bin/backstage-create-app b/packages/create-app/bin/backstage-create-app index 60dfd4990a..4639ea96dc 100755 --- a/packages/create-app/bin/backstage-create-app +++ b/packages/create-app/bin/backstage-create-app @@ -25,7 +25,7 @@ if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) { require('..'); } else { // Only used for development, so should be a devDependency - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-undeclared-imports require('ts-node').register({ transpileOnly: true, project: path.resolve(__dirname, '../../../tsconfig.json'), diff --git a/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json b/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json index 2b3a458b95..a467608916 100644 --- a/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json +++ b/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } diff --git a/packages/e2e-test/.eslintrc.js b/packages/e2e-test/.eslintrc.js index f9fb2e27c9..e2a53a6ad2 100644 --- a/packages/e2e-test/.eslintrc.js +++ b/packages/e2e-test/.eslintrc.js @@ -1,14 +1 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - 'no-console': 0, - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: false, - peerDependencies: false, - bundledDependencies: false, - }, - ], - }, -}); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 374eaa6d18..ee447a1736 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -26,22 +26,24 @@ "clean": "backstage-cli package clean" }, "bin": "bin/e2e-test", - "devDependencies": { - "@backstage/cli": "workspace:^", + "dependencies": { "@backstage/cli-common": "workspace:^", "@backstage/errors": "workspace:^", - "@types/fs-extra": "^9.0.1", - "@types/node": "^16.11.26", - "@types/puppeteer": "^5.4.4", "chalk": "^4.0.0", "commander": "^9.1.0", "cross-fetch": "^3.1.5", "fs-extra": "10.1.0", "handlebars": "^4.7.3", - "nodemon": "^2.0.2", "pgtools": "^0.3.0", "puppeteer": "^17.0.0", - "tree-kill": "^1.2.2", + "tree-kill": "^1.2.2" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^9.0.1", + "@types/node": "^16.11.26", + "@types/puppeteer": "^5.4.4", + "nodemon": "^2.0.2", "ts-node": "^10.0.0" }, "nodemonConfig": { diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 3eedb90b21..254198ebb4 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -159,7 +159,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { appendDeps(pkg); } - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-forbidden-package-imports appendDeps(require('@backstage/create-app/package.json')); print(`Preparing workspace`); diff --git a/packages/repo-tools/src/commands/type-deps/type-deps.ts b/packages/repo-tools/src/commands/type-deps/type-deps.ts index d4dd28aa14..4f8e5e5529 100644 --- a/packages/repo-tools/src/commands/type-deps/type-deps.ts +++ b/packages/repo-tools/src/commands/type-deps/type-deps.ts @@ -17,7 +17,7 @@ import fs from 'fs'; import { resolve as resolvePath } from 'path'; // Cba polluting root package.json, we'll have this -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import chalk from 'chalk'; import { getPackages, Package } from '@manypkg/get-packages'; diff --git a/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json b/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json index 2b3a458b95..a467608916 100644 --- a/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json +++ b/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } diff --git a/packages/techdocs-cli/cypress/.eslintrc.json b/packages/techdocs-cli/cypress/.eslintrc.json index 2481ac5715..06965a82da 100644 --- a/packages/techdocs-cli/cypress/.eslintrc.json +++ b/packages/techdocs-cli/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains", "cy.document"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/run.ts b/plugins/catalog-backend-module-incremental-ingestion/src/run.ts index 08d95f4417..2a58227862 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/run.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/run.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import { databaseFactory, discoveryFactory, diff --git a/plugins/code-climate/dev/index.tsx b/plugins/code-climate/dev/index.tsx index d5fc4c28e3..060679337e 100644 --- a/plugins/code-climate/dev/index.tsx +++ b/plugins/code-climate/dev/index.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import { createDevApp, EntityGridItem } from '@backstage/dev-utils'; import { Grid } from '@material-ui/core'; import React from 'react'; diff --git a/plugins/code-climate/src/setupTests.ts b/plugins/code-climate/src/setupTests.ts index 43d9e09713..eb4654b869 100644 --- a/plugins/code-climate/src/setupTests.ts +++ b/plugins/code-climate/src/setupTests.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ import '@testing-library/jest-dom'; import 'cross-fetch/polyfill'; diff --git a/plugins/cost-insights/src/example/templates/CostInsightsClient.ts b/plugins/cost-insights/src/example/templates/CostInsightsClient.ts index 50c3bb1470..046196f26b 100644 --- a/plugins/cost-insights/src/example/templates/CostInsightsClient.ts +++ b/plugins/cost-insights/src/example/templates/CostInsightsClient.ts @@ -22,7 +22,7 @@ // IMPORTANT: Remove the lines below to enable type checking and linting // @ts-nocheck -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ import { CostInsightsApi, diff --git a/plugins/example-todo-list/.eslintrc.js b/plugins/example-todo-list/.eslintrc.js index 8c18e76229..e2a53a6ad2 100644 --- a/plugins/example-todo-list/.eslintrc.js +++ b/plugins/example-todo-list/.eslintrc.js @@ -1,14 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], - rules: { - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], - }, -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend/scripts/build-nunjucks.js b/plugins/scaffolder-backend/scripts/build-nunjucks.js index 8b98ac4305..8183de77ee 100755 --- a/plugins/scaffolder-backend/scripts/build-nunjucks.js +++ b/plugins/scaffolder-backend/scripts/build-nunjucks.js @@ -16,7 +16,7 @@ */ /* eslint-disable no-restricted-syntax */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const path = require('path'); diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts b/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts index 764ac94e2c..9beb4e150e 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts +++ b/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import { act, fireEvent, diff --git a/scripts/assemble-manifest.js b/scripts/assemble-manifest.js index f68a83447d..f01bc7fd9b 100755 --- a/scripts/assemble-manifest.js +++ b/scripts/assemble-manifest.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2022 The Backstage Authors * diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index 10c4b99976..3dcbd6ef81 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -56,7 +56,7 @@ async function listFiles(dir = '') { // caused by the script. In CI, we want to ensure vale linter is run. async function exitIfMissingVale() { try { - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-undeclared-imports await require('command-exists')('vale'); } catch (e) { if (process.env.CI) { diff --git a/scripts/check-if-release.js b/scripts/check-if-release.js index e7dabe14a4..c12b27b0ef 100755 --- a/scripts/check-if-release.js +++ b/scripts/check-if-release.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2020 The Backstage Authors * diff --git a/scripts/create-github-release.js b/scripts/create-github-release.js index 4b3891c5ce..b64d7a9cb5 100755 --- a/scripts/create-github-release.js +++ b/scripts/create-github-release.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2020 The Backstage Authors * diff --git a/scripts/create-release-tag.js b/scripts/create-release-tag.js index 28417024fd..311b46d4e5 100755 --- a/scripts/create-release-tag.js +++ b/scripts/create-release-tag.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2020 The Backstage Authors * diff --git a/scripts/list-deprecations.js b/scripts/list-deprecations.js index 8047158138..22aff96d67 100755 --- a/scripts/list-deprecations.js +++ b/scripts/list-deprecations.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const _ = require('lodash'); const fs = require('fs-extra'); diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index 973f4fad58..4019a71cf3 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2022 The Backstage Authors * diff --git a/scripts/snyk-github-issue-sync.ts b/scripts/snyk-github-issue-sync.ts index da64e2ea82..ea7d42a2a4 100755 --- a/scripts/snyk-github-issue-sync.ts +++ b/scripts/snyk-github-issue-sync.ts @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ import { Octokit } from '@octokit/rest'; import minimist from 'minimist'; // Generated by GitHub workflow .github/workflows/snyk-github-issue-creator diff --git a/scripts/verify-api-reference.js b/scripts/verify-api-reference.js index 84a526604d..091e803c33 100755 --- a/scripts/verify-api-reference.js +++ b/scripts/verify-api-reference.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { resolve: resolvePath } = require('path'); const { promises: fs } = require('fs'); diff --git a/scripts/verify-changesets.js b/scripts/verify-changesets.js index 9e3064eeac..ae3f07840a 100755 --- a/scripts/verify-changesets.js +++ b/scripts/verify-changesets.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { resolve: resolvePath } = require('path'); const fs = require('fs-extra'); diff --git a/scripts/verify-links.js b/scripts/verify-links.js index 05ad387c37..7664cadad5 100755 --- a/scripts/verify-links.js +++ b/scripts/verify-links.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { resolve: resolvePath, join: joinPath, dirname } = require('path'); const fs = require('fs').promises; diff --git a/scripts/verify-lockfile-duplicates.js b/scripts/verify-lockfile-duplicates.js index cc22664219..820fba2ea5 100644 --- a/scripts/verify-lockfile-duplicates.js +++ b/scripts/verify-lockfile-duplicates.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { execFile: execFileCb } = require('child_process'); const { resolve: resolvePath, dirname: dirnamePath } = require('path'); From 4753d26b949a521c48334882c6bac4eb6cd2a2c3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 16:42:23 +0100 Subject: [PATCH 27/90] eslint-plugin: add no-relative-monorepo-imports rule Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/index.js | 2 + packages/eslint-plugin/lib/getPackages.js | 10 ++- .../rules/no-relative-monorepo-imports.js | 80 +++++++++++++++++++ .../rules/no-undeclared-imports.js | 4 +- 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 packages/eslint-plugin/rules/no-relative-monorepo-imports.js diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js index cf1dd20ab7..20a0ff2d18 100644 --- a/packages/eslint-plugin/index.js +++ b/packages/eslint-plugin/index.js @@ -20,12 +20,14 @@ module.exports = { plugins: ['@backstage'], rules: { '@backstage/no-forbidden-package-imports': 'error', + '@backstage/no-relative-monorepo-imports': 'error', '@backstage/no-undeclared-imports': 'error', }, }, }, rules: { 'no-forbidden-package-imports': require('./rules/no-forbidden-package-imports'), + 'no-relative-monorepo-imports': require('./rules/no-relative-monorepo-imports'), 'no-undeclared-imports': require('./rules/no-undeclared-imports'), }, }; diff --git a/packages/eslint-plugin/lib/getPackages.js b/packages/eslint-plugin/lib/getPackages.js index 318d0654c2..6c8c1a66b2 100644 --- a/packages/eslint-plugin/lib/getPackages.js +++ b/packages/eslint-plugin/lib/getPackages.js @@ -16,7 +16,7 @@ // @ts-check -/** @type {import('@manypkg/get-packages')} */ +const path = require('path'); const manypkg = require('@manypkg/get-packages'); /** @@ -31,6 +31,7 @@ const manypkg = require('@manypkg/get-packages'); * @property {ExtendedPackage} root * @property {ExtendedPackage[]} list * @property {Map} map + * @property {(path: string) => ExtendedPackage | undefined} byPath */ // Loads all packages in the monorepo once, and caches the result @@ -56,6 +57,13 @@ module.exports = (function () { map: new Map(packages.packages.map(pkg => [pkg.packageJson.name, pkg])), list: packages.packages, root: packages.root, + byPath(filePath) { + return packages.packages.find(pkg => { + if (!path.relative(pkg.dir, filePath).startsWith('..')) { + return pkg; + } + }); + }, }; lastLoadAt = Date.now(); return result; diff --git a/packages/eslint-plugin/rules/no-relative-monorepo-imports.js b/packages/eslint-plugin/rules/no-relative-monorepo-imports.js new file mode 100644 index 0000000000..cb18eb506c --- /dev/null +++ b/packages/eslint-plugin/rules/no-relative-monorepo-imports.js @@ -0,0 +1,80 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const path = require('path'); +const visitImports = require('../lib/visitImports'); +const getPackageMap = require('../lib/getPackages'); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + messages: { + outside: 'Import of {{path}} is outside of any known monorepo package', + forbidden: + "Relative imports of monorepo packages are forbidden, use '{{newImport}}' instead", + }, + }, + create(context) { + const packages = getPackageMap(context.getCwd()); + if (!packages) { + return {}; + } + const filePath = context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename(); + + const localPkg = packages.byPath(filePath); + if (!localPkg) { + return {}; + } + + return visitImports(context, (node, imp) => { + if (imp.type !== 'local') { + return; + } + + const target = path.resolve(path.dirname(filePath), imp.path); + if (!path.relative(localPkg.dir, target).startsWith('..')) { + return; + } + + const targetPkg = packages.byPath(target); + if (!targetPkg) { + context.report({ + node: node, + messageId: 'outside', + data: { + path: target, + }, + }); + return; + } + + const targetPath = path.relative(targetPkg.dir, target); + const targetName = targetPkg.packageJson.name ?? ''; + context.report({ + node: node, + messageId: 'forbidden', + data: { + newImport: targetPath ? `${targetName}/${targetPath}` : targetName, + }, + }); + }); + }, +}; diff --git a/packages/eslint-plugin/rules/no-undeclared-imports.js b/packages/eslint-plugin/rules/no-undeclared-imports.js index f7f7b17354..4a8756b71e 100644 --- a/packages/eslint-plugin/rules/no-undeclared-imports.js +++ b/packages/eslint-plugin/rules/no-undeclared-imports.js @@ -135,9 +135,7 @@ module.exports = { ? context.getPhysicalFilename() : context.getFilename(); - const localPkg = packages.list.find(p => - filePath.startsWith(p.dir + path.sep), - ); + const localPkg = packages.byPath(filePath); if (!localPkg) { return {}; } From 906af5db533ee9356b61a7f3844339d3d77cbbb4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 16:48:43 +0100 Subject: [PATCH 28/90] cli: remove unnecessary lint rules and fix issues Signed-off-by: Patrik Oldsberg --- packages/cli/config/eslint-factory.js | 8 +------- packages/cli/package.json | 1 - packages/cli/src/lib/version.ts | 2 +- packages/core-plugin-api/src/app/types.ts | 2 +- packages/create-app/src/index.ts | 1 + packages/create-app/src/lib/versions.ts | 2 +- 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index f344af4ab0..dde853fdff 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -64,7 +64,7 @@ function createConfig(dir, extraConfig = {}) { ...(extraExtends ?? []), ], parser: '@typescript-eslint/parser', - plugins: ['import', 'monorepo', ...(plugins ?? [])], + plugins: ['import', ...(plugins ?? [])], env: { jest: true, ...env, @@ -87,7 +87,6 @@ function createConfig(dir, extraConfig = {}) { '@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-redeclare': 'error', 'no-undef': 'off', - 'monorepo/no-relative-import': 'error', 'import/newline-after-import': 'error', 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', @@ -110,9 +109,6 @@ function createConfig(dir, extraConfig = {}) { ...(restrictedSrcImports ?? []), ], patterns: [ - // Avoid cross-package imports - '**/../../**/*/src/**', - '**/../../**/*/src', // Prevent imports of stories or tests '*.stories*', '*.test*', @@ -162,8 +158,6 @@ function createConfig(dir, extraConfig = {}) { ...(restrictedImports ?? []), ...(restrictedTestImports ?? []), ], - // Avoid cross-package imports - patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], }, ], }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 909ac8460c..359f16adfa 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -76,7 +76,6 @@ "eslint-plugin-import": "^2.25.4", "eslint-plugin-jest": "^27.0.0", "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-monorepo": "^0.3.2", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", "eslint-webpack-plugin": "^3.1.1", diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index 5a199236f5..bf89842d5f 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -19,7 +19,7 @@ import semver from 'semver'; import { paths } from './paths'; import { Lockfile } from './versioning'; -/* eslint-disable monorepo/no-relative-import */ +/* eslint-disable @backstage/no-relative-monorepo-imports */ /* This is a list of all packages used by the templates. If dependencies are added or removed, diff --git a/packages/core-plugin-api/src/app/types.ts b/packages/core-plugin-api/src/app/types.ts index 2a71eda1f4..f3e0547ca4 100644 --- a/packages/core-plugin-api/src/app/types.ts +++ b/packages/core-plugin-api/src/app/types.ts @@ -17,7 +17,7 @@ // This is a bit of a hack that we use to avoid having to redeclare these types // within this package or have an explicit dependency on core-app-api. // These types end up being inlined and duplicated into this package at build time. -// eslint-disable-next-line no-restricted-imports +// eslint-disable-next-line @backstage/no-relative-monorepo-imports export type { BootErrorPageProps, SignInPageProps, diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index 216fab6740..fa235e6175 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -22,6 +22,7 @@ import { program } from 'commander'; import { exitWithError } from './lib/errors'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { version } from '../../../package.json'; import createApp from './createApp'; diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index b89b1d5463..72f5c2c9d5 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -/* eslint-disable monorepo/no-relative-import */ +/* eslint-disable @backstage/no-relative-monorepo-imports */ /* This is a list of all packages used by the template. If dependencies are added or removed, From ef73e8626bf906febc682f1f31451a4668e329c3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 17:04:24 +0100 Subject: [PATCH 29/90] eslint-plugin: add docs for no-undeclared-imports Signed-off-by: Patrik Oldsberg --- .../docs/rules/no-undeclared-imports.md | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-undeclared-imports.md diff --git a/packages/eslint-plugin/docs/rules/no-undeclared-imports.md b/packages/eslint-plugin/docs/rules/no-undeclared-imports.md new file mode 100644 index 0000000000..6102c772e6 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-undeclared-imports.md @@ -0,0 +1,84 @@ +# @backstage/no-undeclared-imports + +Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`. + +## Usage + +Add the rules as follows, it has no options: + +```js +"@backstage/no-undeclared-imports": ["error"] +``` + +The following patterns are considered files used during development, and only need dependencies to be declared in devDependencies: + +```python +!src/** # Any files outside of src are considered dev files +src/**/*.test.* +src/**/*.stories.* +src/**/__testUtils__/** +src/**/__mocks__/** +src/setupTests.* +``` + +## Rule Details + +Given the following `package.json`: + +```json +{ + "name": "@backstage/plugin-foo", + "backstage": { + "role": "frontend-plugin" + }, + "dependencies": { + "react": "^17.0.0" + }, + "devDependencies": { + "@backstage/core-plugin-api": "^1.0.0" + }, + "peerDependencies": { + "@backstage/config": "*" + } +} +``` + +### Fail + +Inside `src/my-plugin.ts`: + +```ts +// Should be declared as a dependency +const _ = require('lodash'); +import _ from 'lodash'; + +// React should be a peer dependency in frontend plugins +import react from 'react'; + +// Should be declared as a dependency, not a dev dependency +import { useApi } from '@backstage/core-plugin-api'; +``` + +Inside `src/my-plugin.test.ts` (a test file): + +```ts +// Should be declared as a dev dependency +const _ = require('lodash'); +import _ from 'lodash'; +``` + +### Pass + +Inside `src/my-plugin.ts`: + +```ts +// Declared in peerDependencies, so it is allowed +import { ConfigReader } from '@backstage/config'; +``` + +Inside `src/my-plugin.test.ts` (a test file): + +```ts +// Declared as a dev dependency inside a test file +import { useApi } from '@backstage/core-plugin-api'; +``` From 5105b9848a6ed57512697f3c80d198dcdabfdbae Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 17:10:51 +0100 Subject: [PATCH 30/90] eslint-plugin: add docs for no-relative-monorepo-imports Signed-off-by: Patrik Oldsberg --- .../rules/no-relative-monorepo-imports.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md diff --git a/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md b/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md new file mode 100644 index 0000000000..6a41a73fb6 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md @@ -0,0 +1,43 @@ +# @backstage/no-relative-monorepo-imports + +Forbid relative imports that reach outside of the package in a monorepo. + +## Usage + +Add the rules as follows, it has no options: + +```js +"@backstage/no-relative-monorepo-imports": ["error"] +``` + +The following patterns are considered files used during development, and only need dependencies to be declared in devDependencies: + +```python +!src/** # Any files outside of src are considered dev files +src/**/*.test.* +src/**/*.stories.* +src/**/__testUtils__/** +src/**/__mocks__/** +src/setupTests.* +``` + +## Rule Details + +Assuming an import from for example `plugins/bar/src/index.ts`: + +### Fail + +```ts +import { FooCard } from '../../foo'; + +import { FooCard } from '../../foo/src/components/FooCard'; +``` + +### Pass + +```ts +import { FooCard } from '@internal/plugin-foo'; + +// This is allowed by this rule, but not by no-forbidden-package-imports +import { FooCard } from '@internal/plugin-foo/src/components/FooCard'; +``` From 015c0e0f89025c7ad8925a2bf7ad5321efaf4adf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 17:15:55 +0100 Subject: [PATCH 31/90] eslint-plugin: add docs for no-forbidden-package-imports Signed-off-by: Patrik Oldsberg --- .../rules/no-forbidden-package-imports.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md diff --git a/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md b/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md new file mode 100644 index 0000000000..7a6ae11c37 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md @@ -0,0 +1,52 @@ +# @backstage/no-forbidden-package-imports + +Disallow internal monorepo imports from package subpaths that are not exported. + +## Usage + +Add the rules as follows, it has no options: + +```js +"@backstage/no-forbidden-package-imports": ["error"] +``` + +## Rule Details + +Given the following two target packages: + +```json +{ + "name": "@backstage/plugin-foo", + "files": ["dist", "type-utils"] +} +``` + +```json +{ + "name": "@backstage/plugin-bar", + "exports": { + ".": "./src/index.ts", + "./testUtils": "./src/testUtils/index.ts", + "./package.json": "./package.json" + } +} +``` + +### Fail + +```ts +import { FooCard } from '@backstage/plugin-foo/src/components'; +import { BarCard } from '@backstage/plugin-bar/src/components'; +``` + +### Pass + +```ts +import { FooCard } from '@backstage/plugin-foo'; +import { FooType } from '@backstage/plugin-foo/type-utils'; +import fooPkg from '@backstage/plugin-foo/package.json'; + +import { BarCard } from '@backstage/plugin-bar'; +import { renderBarCardExtension } from '@backstage/plugin-bar/testUtils'; +import barPkg from '@backstage/plugin-bar/package.json'; +``` From 71bf2eb0ed63c7fc3482d3d043e055ac865cb418 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 17:20:24 +0100 Subject: [PATCH 32/90] eslint-plugin: add doc rule links and update README Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/README.md | 8 +++++--- .../eslint-plugin/rules/no-forbidden-package-imports.js | 5 +++++ .../eslint-plugin/rules/no-relative-monorepo-imports.js | 5 +++++ packages/eslint-plugin/rules/no-undeclared-imports.js | 5 +++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index a063d801d3..c311670603 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -35,6 +35,8 @@ rules: { The following rules are provided by this plugin: -| Rule | Description | -| ----------------------------------------- | ------------------------------------------------------------------------------- | -| `@backstage/no-forbidden-package-imports` | Disallow internal monorepo imports from package subpaths that are not exported. | +| Rule | Description | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| [@backstage/no-forbidden-package-imports](./docs/rules/no-forbidden-package-imports.md) | Disallow internal monorepo imports from package subpaths that are not exported. | +| [@backstage/no-relative-monorepo-imports](./docs/rules/no-relative-monorepo-imports.md) | Forbid relative imports that reach outside of the package in a monorepo. | +| [@backstage/no-undeclared-imports](./docs/rules/no-undeclared-imports.md) | Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`. | diff --git a/packages/eslint-plugin/rules/no-forbidden-package-imports.js b/packages/eslint-plugin/rules/no-forbidden-package-imports.js index 6c861e17f5..e7c1acca27 100644 --- a/packages/eslint-plugin/rules/no-forbidden-package-imports.js +++ b/packages/eslint-plugin/rules/no-forbidden-package-imports.js @@ -25,6 +25,11 @@ module.exports = { messages: { forbidden: '{{packageName}} does not export {{subPath}}', }, + docs: { + description: + 'Disallow internal monorepo imports from package subpaths that are not exported.', + url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md', + }, }, create(context) { return visitImports(context, (node, imp) => { diff --git a/packages/eslint-plugin/rules/no-relative-monorepo-imports.js b/packages/eslint-plugin/rules/no-relative-monorepo-imports.js index cb18eb506c..df30d35f67 100644 --- a/packages/eslint-plugin/rules/no-relative-monorepo-imports.js +++ b/packages/eslint-plugin/rules/no-relative-monorepo-imports.js @@ -29,6 +29,11 @@ module.exports = { forbidden: "Relative imports of monorepo packages are forbidden, use '{{newImport}}' instead", }, + docs: { + description: + 'Forbid relative imports that reach outside of the package in a monorepo.', + url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md', + }, }, create(context) { const packages = getPackageMap(context.getCwd()); diff --git a/packages/eslint-plugin/rules/no-undeclared-imports.js b/packages/eslint-plugin/rules/no-undeclared-imports.js index 4a8756b71e..4f60b712d9 100644 --- a/packages/eslint-plugin/rules/no-undeclared-imports.js +++ b/packages/eslint-plugin/rules/no-undeclared-imports.js @@ -125,6 +125,11 @@ module.exports = { switch: '{{ packageName }} is declared in {{ oldDepsField }}, but should be moved to {{ depsField }} in {{ packageJsonPath }}.', }, + docs: { + description: + 'Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`.', + url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-undeclared-imports.md', + }, }, create(context) { const packages = getPackageMap(context.getCwd()); From dd8a9afe668ab0bb69bc2d65130e513ce58e8426 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 17:24:49 +0100 Subject: [PATCH 33/90] added and updated changesets for eslint-plugin addition Signed-off-by: Patrik Oldsberg --- .changeset/few-moons-accept.md | 5 +++++ .changeset/tough-jeans-camp.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/few-moons-accept.md diff --git a/.changeset/few-moons-accept.md b/.changeset/few-moons-accept.md new file mode 100644 index 0000000000..c790e837c8 --- /dev/null +++ b/.changeset/few-moons-accept.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Replaced several monorepo lint rules with new rules from `@backstage/eslint-plugin`. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for a full list of rules. diff --git a/.changeset/tough-jeans-camp.md b/.changeset/tough-jeans-camp.md index b7c08593e1..8c7a53190f 100644 --- a/.changeset/tough-jeans-camp.md +++ b/.changeset/tough-jeans-camp.md @@ -2,4 +2,4 @@ '@backstage/eslint-plugin': minor --- -Added a new ESLint plugin with common rules for Backstage projects. +Added a new ESLint plugin with common rules for Backstage projects. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for more details. From 1b30c9a0f7f77a16e16c238c324b6c6bf4f3b1d4 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 3 Feb 2023 19:59:51 +0100 Subject: [PATCH 34/90] feat(search): improve router query errors Signed-off-by: Camila Belo --- .changeset/hip-onions-tan.md | 5 +++++ .../search-backend/src/service/router.test.ts | 17 +++++++++++++++++ plugins/search-backend/src/service/router.ts | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .changeset/hip-onions-tan.md diff --git a/.changeset/hip-onions-tan.md b/.changeset/hip-onions-tan.md new file mode 100644 index 0000000000..01be0d4867 --- /dev/null +++ b/.changeset/hip-onions-tan.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend': patch +--- + +Change the router's response to include the error message instead of its object type in case it fails during a search query because the messages have more info. diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 1dbd145bce..b4772dd62d 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -74,6 +74,23 @@ describe('createRouter', () => { }); describe('GET /query', () => { + it('throws meaningful query errors', async () => { + const error = new Error('Query error message'); + mockSearchEngine.query.mockRejectedValueOnce(error); + + const response = await request(app).get('/query'); + + expect(response.status).toEqual(500); + expect(response.body).toMatchObject( + expect.objectContaining({ + error: { + name: 'Error', + message: `There was a problem performing the search query: ${error.message}`, + }, + }), + ); + }); + it('returns empty results array', async () => { const response = await request(app).get('/query'); diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index b51dcead14..10f47d180f 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -184,7 +184,7 @@ export async function createRouter( } throw new Error( - `There was a problem performing the search query. ${error}`, + `There was a problem performing the search query: ${error.message}`, ); } }, From d5fe9f10934ad24fee2b5db055ee502f0ee349bb Mon Sep 17 00:00:00 2001 From: Mark Anderson-Trocme Date: Fri, 3 Feb 2023 16:09:28 -0500 Subject: [PATCH 35/90] feature: build workflow url Signed-off-by: Mark Anderson-Trocme --- .../src/components/BuildsPage/lib/CITable/CITable.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx index 8f99f1f69c..6b22d641ee 100644 --- a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -214,7 +214,14 @@ const generatedColumns: TableColumn[] = [ field: 'workflow.name', highlight: true, render: (row: Partial) => ( - {row?.workflow?.name} + + {row?.workflow?.name} + ), }, { From c044a3629b6549981c0b6ae50561cef9bf21ba60 Mon Sep 17 00:00:00 2001 From: Mark Anderson-Trocme Date: Fri, 3 Feb 2023 16:16:47 -0500 Subject: [PATCH 36/90] lint Signed-off-by: Mark Anderson-Trocme --- .../src/components/BuildsPage/lib/CITable/CITable.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx index 6b22d641ee..22f69ac30f 100644 --- a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -215,10 +215,7 @@ const generatedColumns: TableColumn[] = [ highlight: true, render: (row: Partial) => ( {row?.workflow?.name} From aefe3a562495c2a1e8efbb926958654d85f0e4fb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 18:06:52 +0100 Subject: [PATCH 37/90] eslint-plugin: added tests for no-relative-monorepo-imports Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/.eslintrc.js | 3 + packages/eslint-plugin/package.json | 6 +- .../src/__fixtures__/monorepo/package.json | 6 + .../monorepo/packages/bar/package.json | 9 ++ .../monorepo/packages/foo/package.json | 7 + .../src/no-relative-monorepo-imports.test.ts | 68 +++++++++ yarn.lock | 140 +----------------- 7 files changed, 102 insertions(+), 137 deletions(-) create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/package.json create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json create mode 100644 packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json create mode 100644 packages/eslint-plugin/src/no-relative-monorepo-imports.test.ts diff --git a/packages/eslint-plugin/.eslintrc.js b/packages/eslint-plugin/.eslintrc.js index 7fc68c3c5d..01d1b181d7 100644 --- a/packages/eslint-plugin/.eslintrc.js +++ b/packages/eslint-plugin/.eslintrc.js @@ -21,6 +21,9 @@ module.exports = { node: true, es2021: true, }, + parserOptions: { + sourceType: 'module', + }, rules: { '@backstage/no-undeclared-imports': ['error'], 'no-unused-expressions': 'off', diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 73862d2295..84fc16b98f 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -14,13 +14,15 @@ "license": "Apache-2.0", "main": "./index.js", "scripts": { - "lint": "backstage-cli package lint" + "lint": "backstage-cli package lint", + "test": "backstage-cli package test" }, "dependencies": { "@manypkg/get-packages": "^1.1.3", "minimatch": "^5.1.2" }, "devDependencies": { - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "eslint": "^8.33.0" } } diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/package.json new file mode 100644 index 0000000000..c35178c46f --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/package.json @@ -0,0 +1,6 @@ +{ + "name": "root", + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json new file mode 100644 index 0000000000..8331b20c43 --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json @@ -0,0 +1,9 @@ +{ + "name": "@internal/bar", + "backstage": { + "role": "frontend-plugin" + }, + "peerDependencies": { + "react": "*" + } +} diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json new file mode 100644 index 0000000000..66341563cd --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json @@ -0,0 +1,7 @@ +{ + "name": "@internal/foo", + "dependencies": { + "@internal/bar": "1.0.0", + "lodash": "*" + } +} diff --git a/packages/eslint-plugin/src/no-relative-monorepo-imports.test.ts b/packages/eslint-plugin/src/no-relative-monorepo-imports.test.ts new file mode 100644 index 0000000000..6888d258cc --- /dev/null +++ b/packages/eslint-plugin/src/no-relative-monorepo-imports.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RuleTester } from 'eslint'; +import path from 'path'; +import rule from '../rules/no-relative-monorepo-imports'; + +const RULE = 'no-relative-monorepo-imports'; +const FIXTURE = path.resolve(__dirname, '__fixtures__/monorepo'); + +const ERR_OUTSIDE = (path: string) => ({ + message: `Import of ${path} is outside of any known monorepo package`, +}); +const ERR_FORBIDDEN = (newImp: string) => ({ + message: `Relative imports of monorepo packages are forbidden, use '${newImp}' instead`, +}); + +process.chdir(FIXTURE); + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + ecmaVersion: 2021, + }, +}); + +ruleTester.run(RULE, rule, { + valid: [ + { + code: `import { version } from '@internal/foo'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + }, + { + code: `import { version } from '@internal/foo/src'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + }, + ], + invalid: [ + { + code: `import { version } from '../../foo'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_FORBIDDEN('@internal/foo')], + }, + { + code: `import { version } from '../../foo/src'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_FORBIDDEN('@internal/foo/src')], + }, + { + code: `import { version } from '../../../package.json'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_OUTSIDE(path.join(FIXTURE, 'package.json'))], + }, + ], +}); diff --git a/yarn.lock b/yarn.lock index e0e1da349c..2d250e9913 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3721,7 +3721,6 @@ __metadata: eslint-plugin-import: ^2.25.4 eslint-plugin-jest: ^27.0.0 eslint-plugin-jsx-a11y: ^6.5.1 - eslint-plugin-monorepo: ^0.3.2 eslint-plugin-react: ^7.28.0 eslint-plugin-react-hooks: ^4.3.0 eslint-webpack-plugin: ^3.1.1 @@ -4101,6 +4100,7 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@manypkg/get-packages": ^1.1.3 + eslint: ^8.33.0 minimatch: ^5.1.2 languageName: unknown linkType: soft @@ -16670,15 +16670,6 @@ __metadata: languageName: node linkType: hard -"array-union@npm:^1.0.1": - version: 1.0.2 - resolution: "array-union@npm:1.0.2" - dependencies: - array-uniq: ^1.0.1 - checksum: 82cec6421b6e6766556c484835a6d476a873f1b71cace5ab2b4f1b15b1e3162dc4da0d16f7a2b04d4aec18146c6638fe8f661340b31ba8e469fd811a1b45dc8d - languageName: node - linkType: hard - "array-union@npm:^2.1.0": version: 2.1.0 resolution: "array-union@npm:2.1.0" @@ -16686,13 +16677,6 @@ __metadata: languageName: node linkType: hard -"array-uniq@npm:^1.0.1": - version: 1.0.3 - resolution: "array-uniq@npm:1.0.3" - checksum: 1625f06b093d8bf279b81adfec6e72951c0857d65b5e3f65f053fffe9f9dd61c2fc52cff57e38a4700817e7e3f01a4faa433d505ea9e33cdae4514c334e0bf9e - languageName: node - linkType: hard - "array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.3.1": version: 1.3.1 resolution: "array.prototype.flat@npm:1.3.1" @@ -20534,15 +20518,6 @@ __metadata: languageName: node linkType: hard -"dir-glob@npm:^2.0.0": - version: 2.2.2 - resolution: "dir-glob@npm:2.2.2" - dependencies: - path-type: ^3.0.0 - checksum: 3aa48714a9f7845ffc30ab03a5c674fe760477cc55e67b0847333371549227d93953e6627ec160f75140c5bea5c5f88d13c01de79bd1997a588efbcf06980842 - languageName: node - linkType: hard - "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -21606,7 +21581,7 @@ __metadata: languageName: node linkType: hard -"eslint-module-utils@npm:^2.1.1, eslint-module-utils@npm:^2.7.4": +"eslint-module-utils@npm:^2.7.4": version: 2.7.4 resolution: "eslint-module-utils@npm:2.7.4" dependencies: @@ -21711,21 +21686,6 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-monorepo@npm:^0.3.2": - version: 0.3.2 - resolution: "eslint-plugin-monorepo@npm:0.3.2" - dependencies: - eslint-module-utils: ^2.1.1 - get-monorepo-packages: ^1.1.0 - globby: ^7.1.1 - load-json-file: ^4.0.0 - minimatch: ^3.0.4 - parse-package-name: ^0.1.0 - path-is-inside: ^1.0.2 - checksum: b6f17efbc9e66aefbf7aacf9aa130bc8e729a6311c412d1b0026ce7d93361c10b0e6e4b10cf434f24ca9a8b2e891bc8e5b82e66dc9ccc5e782f9c18a989eca28 - languageName: node - linkType: hard - "eslint-plugin-notice@npm:^0.9.10": version: 0.9.10 resolution: "eslint-plugin-notice@npm:0.9.10" @@ -21845,7 +21805,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.6.0": +"eslint@npm:^8.33.0, eslint@npm:^8.6.0": version: 8.33.0 resolution: "eslint@npm:8.33.0" dependencies: @@ -23438,16 +23398,6 @@ __metadata: languageName: node linkType: hard -"get-monorepo-packages@npm:^1.1.0": - version: 1.2.0 - resolution: "get-monorepo-packages@npm:1.2.0" - dependencies: - globby: ^7.1.1 - load-json-file: ^4.0.0 - checksum: f9321c11b8e11f02138758db6589d8ab8b7e1b05e78cac92493b635faea10aa100c20fe40f2cf110c82ec100c118c6c131dff4c65d32a721c617dc2928f7b277 - languageName: node - linkType: hard - "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -23598,7 +23548,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7, glob@npm:^7.2.0": +"glob@npm:^7.0.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7, glob@npm:^7.2.0": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -23707,20 +23657,6 @@ __metadata: languageName: node linkType: hard -"globby@npm:^7.1.1": - version: 7.1.1 - resolution: "globby@npm:7.1.1" - dependencies: - array-union: ^1.0.1 - dir-glob: ^2.0.0 - glob: ^7.1.2 - ignore: ^3.3.5 - pify: ^3.0.0 - slash: ^1.0.0 - checksum: f0eba08a08ae7c98149a4411661c0bf08c4717d81e6f355cf624fb01880b249737eb8e951bf86124cb3af8ea1c793c0a9d363ed5cdec99bb2c6b68f8a323025f - languageName: node - linkType: hard - "good-listener@npm:^1.2.2": version: 1.2.2 resolution: "good-listener@npm:1.2.2" @@ -24710,13 +24646,6 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^3.3.5": - version: 3.3.10 - resolution: "ignore@npm:3.3.10" - checksum: 23e8cc776e367b56615ab21b78decf973a35dfca5522b39d9b47643d8168473b0d1f18dd1321a1bab466a12ea11a2411903f3b21644f4d5461ee0711ec8678bd - languageName: node - linkType: hard - "ignore@npm:^5.1.4, ignore@npm:^5.2.0": version: 5.2.0 resolution: "ignore@npm:5.2.0" @@ -26828,13 +26757,6 @@ __metadata: languageName: node linkType: hard -"json-parse-better-errors@npm:^1.0.1": - version: 1.0.2 - resolution: "json-parse-better-errors@npm:1.0.2" - checksum: ff2b5ba2a70e88fd97a3cb28c1840144c5ce8fae9cbeeddba15afa333a5c407cf0e42300cd0a2885dbb055227fe68d405070faad941beeffbfde9cf3b2c78c5d - languageName: node - linkType: hard - "json-parse-even-better-errors@npm:^2.3.0, json-parse-even-better-errors@npm:^2.3.1": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -27725,18 +27647,6 @@ __metadata: languageName: node linkType: hard -"load-json-file@npm:^4.0.0": - version: 4.0.0 - resolution: "load-json-file@npm:4.0.0" - dependencies: - graceful-fs: ^4.1.2 - parse-json: ^4.0.0 - pify: ^3.0.0 - strip-bom: ^3.0.0 - checksum: 8f5d6d93ba64a9620445ee9bde4d98b1eac32cf6c8c2d20d44abfa41a6945e7969456ab5f1ca2fb06ee32e206c9769a20eec7002fe290de462e8c884b6b8b356 - languageName: node - linkType: hard - "load-yaml-file@npm:^0.2.0": version: 0.2.0 resolution: "load-yaml-file@npm:0.2.0" @@ -30849,16 +30759,6 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^4.0.0": - version: 4.0.0 - resolution: "parse-json@npm:4.0.0" - dependencies: - error-ex: ^1.3.1 - json-parse-better-errors: ^1.0.1 - checksum: 0fe227d410a61090c247e34fa210552b834613c006c2c64d9a05cfe9e89cf8b4246d1246b1a99524b53b313e9ac024438d0680f67e33eaed7e6f38db64cfe7b5 - languageName: node - linkType: hard - "parse-json@npm:^5.0.0, parse-json@npm:^5.2.0": version: 5.2.0 resolution: "parse-json@npm:5.2.0" @@ -30871,13 +30771,6 @@ __metadata: languageName: node linkType: hard -"parse-package-name@npm:^0.1.0": - version: 0.1.0 - resolution: "parse-package-name@npm:0.1.0" - checksum: 7d69b1deeaf82d5a59536c5a8772c8db003b1415dd2465073343dfab5caf90917ecfffdd71d1c004f5cf963c3881ccb8899d0ff820eac705a272e817e246de14 - languageName: node - linkType: hard - "parse-path@npm:^7.0.0": version: 7.0.0 resolution: "parse-path@npm:7.0.0" @@ -31150,7 +31043,7 @@ __metadata: languageName: node linkType: hard -"path-is-inside@npm:1.0.2, path-is-inside@npm:^1.0.2": +"path-is-inside@npm:1.0.2": version: 1.0.2 resolution: "path-is-inside@npm:1.0.2" checksum: 0b5b6c92d3018b82afb1f74fe6de6338c4c654de4a96123cb343f2b747d5606590ac0c890f956ed38220a4ab59baddfd7b713d78a62d240b20b14ab801fa02cb @@ -31249,15 +31142,6 @@ __metadata: languageName: node linkType: hard -"path-type@npm:^3.0.0": - version: 3.0.0 - resolution: "path-type@npm:3.0.0" - dependencies: - pify: ^3.0.0 - checksum: 735b35e256bad181f38fa021033b1c33cfbe62ead42bb2222b56c210e42938eecb272ae1949f3b6db4ac39597a61b44edd8384623ec4d79bfdc9a9c0f12537a6 - languageName: node - linkType: hard - "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -31432,13 +31316,6 @@ __metadata: languageName: node linkType: hard -"pify@npm:^3.0.0": - version: 3.0.0 - resolution: "pify@npm:3.0.0" - checksum: 6cdcbc3567d5c412450c53261a3f10991665d660961e06605decf4544a61a97a54fefe70a68d5c37080ff9d6f4cf51444c90198d1ba9f9309a6c0d6e9f5c4fde - languageName: node - linkType: hard - "pify@npm:^4.0.1": version: 4.0.1 resolution: "pify@npm:4.0.1" @@ -34985,13 +34862,6 @@ __metadata: languageName: node linkType: hard -"slash@npm:^1.0.0": - version: 1.0.0 - resolution: "slash@npm:1.0.0" - checksum: 4b6e21b1fba6184a7e2efb1dd173f692d8a845584c1bbf9dc818ff86f5a52fc91b413008223d17cc684604ee8bb9263a420b1182027ad9762e35388434918860 - languageName: node - linkType: hard - "slash@npm:^2.0.0": version: 2.0.0 resolution: "slash@npm:2.0.0" From 6801eb57805c06c398bd82da72f1429acc634a07 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Feb 2023 10:52:55 +0100 Subject: [PATCH 38/90] eslint-plugin: refactor to fix issues uncovered by more strict parsing Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/lib/getPackages.js | 12 ++++++------ packages/eslint-plugin/lib/visitImports.js | 12 ++++++++++-- .../eslint-plugin/rules/no-undeclared-imports.js | 13 +++++++++---- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/lib/getPackages.js b/packages/eslint-plugin/lib/getPackages.js index 6c8c1a66b2..f5f2c63cc1 100644 --- a/packages/eslint-plugin/lib/getPackages.js +++ b/packages/eslint-plugin/lib/getPackages.js @@ -36,8 +36,10 @@ const manypkg = require('@manypkg/get-packages'); // Loads all packages in the monorepo once, and caches the result module.exports = (function () { + /** @type {PackageMap | undefined} */ let result = undefined; - let lastLoadAt = undefined; + /** @type {number} */ + let lastLoadAt = 0; /** @returns {PackageMap | undefined} */ return function getPackages(/** @type {string} */ dir) { @@ -58,11 +60,9 @@ module.exports = (function () { list: packages.packages, root: packages.root, byPath(filePath) { - return packages.packages.find(pkg => { - if (!path.relative(pkg.dir, filePath).startsWith('..')) { - return pkg; - } - }); + return packages.packages.find( + pkg => !path.relative(pkg.dir, filePath).startsWith('..'), + ); }, }; lastLoadAt = Date.now(); diff --git a/packages/eslint-plugin/lib/visitImports.js b/packages/eslint-plugin/lib/visitImports.js index ed16c26c7f..43401bf5f9 100644 --- a/packages/eslint-plugin/lib/visitImports.js +++ b/packages/eslint-plugin/lib/visitImports.js @@ -56,12 +56,17 @@ const getPackages = require('./getPackages'); /** * @callback ImportVisitor - * @param {import('eslint').Rule.Node} node + * @param {ConsideredNode} node * @param {LocalImport | InternalImport | ExternalImport | BuiltinImport} import */ /** - * @param {import('estree').ImportDeclaration | import('estree').ExportAllDeclaration | import('estree').ExportNamedDeclaration | import('estree').ImportExpression | import('estree').SimpleCallExpression} node + * @typedef ConsideredNode + * @type {import('estree').ImportDeclaration | import('estree').ExportAllDeclaration | import('estree').ExportNamedDeclaration | import('estree').ImportExpression | import('estree').SimpleCallExpression} + */ + +/** + * @param {ConsideredNode} node * @returns {undefined | {path: string, kind: 'type' | 'value'}} */ function getImportInfo(node) { @@ -103,6 +108,9 @@ module.exports = function visitImports(context, visitor) { return; } + /** + * @param {ConsideredNode} node + */ function visit(node) { const info = getImportInfo(node); if (!info) { diff --git a/packages/eslint-plugin/rules/no-undeclared-imports.js b/packages/eslint-plugin/rules/no-undeclared-imports.js index 4f60b712d9..85d7266eb8 100644 --- a/packages/eslint-plugin/rules/no-undeclared-imports.js +++ b/packages/eslint-plugin/rules/no-undeclared-imports.js @@ -37,7 +37,7 @@ const devModulePatterns = [ ]; function getExpectedDepType( - localPkg, + /** @type {any} */ localPkg, /** @type {string} */ impPath, /** @type {string} */ modulePath, ) { @@ -58,6 +58,7 @@ function getExpectedDepType( case 'react-router-dom': return 'peer'; } + break; case 'cli': case 'frontend': case 'backend': @@ -100,10 +101,14 @@ function findConflict(pkg, name, expectedType) { return { oldDepsField, depsField }; } + return undefined; } -function getAddFlagForConflict(conflict) { - switch (conflict?.depsField) { +/** + * @param {string} depsField + */ +function getAddFlagForDepsField(depsField) { + switch (depsField) { case depFields.dep: return ''; case depFields.dev: @@ -189,7 +194,7 @@ module.exports = { data: { ...conflict, packagePath, - addFlag: getAddFlagForConflict(conflict), + addFlag: getAddFlagForDepsField(conflict.depsField), packageName: imp.packageName, packageJsonPath: packageJsonPath, }, From e6d210947ece9b5fcaa90698f2b577936db3445c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Feb 2023 11:15:52 +0100 Subject: [PATCH 39/90] eslint-plugin: add tests for no-undeclared-imports + fixes Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/.eslintrc.js | 3 + .../rules/no-undeclared-imports.js | 2 +- .../monorepo/packages/bar/package.json | 7 +- .../monorepo/packages/foo/package.json | 7 +- .../src/no-undeclared-imports.test.ts | 194 ++++++++++++++++++ 5 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 packages/eslint-plugin/src/no-undeclared-imports.test.ts diff --git a/packages/eslint-plugin/.eslintrc.js b/packages/eslint-plugin/.eslintrc.js index 01d1b181d7..6b9de4544e 100644 --- a/packages/eslint-plugin/.eslintrc.js +++ b/packages/eslint-plugin/.eslintrc.js @@ -21,8 +21,11 @@ module.exports = { node: true, es2021: true, }, + parser: '@typescript-eslint/parser', parserOptions: { + ecmaVersion: 2018, sourceType: 'module', + lib: require('@backstage/cli/config/tsconfig.json').compilerOptions.lib, }, rules: { '@backstage/no-undeclared-imports': ['error'], diff --git a/packages/eslint-plugin/rules/no-undeclared-imports.js b/packages/eslint-plugin/rules/no-undeclared-imports.js index 85d7266eb8..42624beb5c 100644 --- a/packages/eslint-plugin/rules/no-undeclared-imports.js +++ b/packages/eslint-plugin/rules/no-undeclared-imports.js @@ -161,7 +161,7 @@ module.exports = { const modulePath = path.relative(localPkg.dir, filePath); const expectedType = getExpectedDepType( - localPkg, + localPkg.packageJson, imp.packageName, modulePath, ); diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json index 8331b20c43..c876e00e32 100644 --- a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json @@ -3,7 +3,10 @@ "backstage": { "role": "frontend-plugin" }, - "peerDependencies": { - "react": "*" + "dependencies": { + "react-router": "*" + }, + "devDependencies": { + "react-router-dom": "*" } } diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json index 66341563cd..8a14cad4d3 100644 --- a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json @@ -1,7 +1,12 @@ { "name": "@internal/foo", "dependencies": { - "@internal/bar": "1.0.0", + "@internal/bar": "1.0.0" + }, + "devDependencies": { "lodash": "*" + }, + "peerDependencies": { + "react": "*" } } diff --git a/packages/eslint-plugin/src/no-undeclared-imports.test.ts b/packages/eslint-plugin/src/no-undeclared-imports.test.ts new file mode 100644 index 0000000000..c156558e8d --- /dev/null +++ b/packages/eslint-plugin/src/no-undeclared-imports.test.ts @@ -0,0 +1,194 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RuleTester } from 'eslint'; +import path from 'path'; +import rule from '../rules/no-undeclared-imports'; + +const RULE = 'no-undeclared-imports'; +const FIXTURE = path.resolve(__dirname, '__fixtures__/monorepo'); + +const ERR_UNDECLARED = ( + name: string, + field: string, + path: string, + flag?: string, +) => ({ + message: `${name} must be declared in ${field} of ${path}/package.json, run 'yarn --cwd ${path} add${ + flag ? ` ${flag}` : '' + } ${name}' from the project root.`, +}); +const ERR_SWITCHED = ( + name: string, + old: string, + field: string, + path: string, +) => ({ + message: `${name} is declared in ${old}, but should be moved to ${field} in ${path}/package.json.`, +}); + +process.chdir(FIXTURE); + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + ecmaVersion: 2021, + }, +}); + +ruleTester.run(RULE, rule, { + valid: [ + { + code: `import '@internal/foo'`, + filename: path.join(FIXTURE, 'packages/foo/src/index.ts'), + }, + { + code: `import '@internal/bar'`, + filename: path.join(FIXTURE, 'packages/foo/src/index.ts'), + }, + { + code: `import 'react'`, + filename: path.join(FIXTURE, 'packages/foo/src/index.ts'), + }, + { + code: `import '@internal/foo'`, + filename: path.join(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + code: `import '@internal/bar'`, + filename: path.join(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + code: `import 'lodash'`, + filename: path.join(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + code: `import 'react'`, + filename: path.join(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + // We're only able to validate literals + code: `require('lod' + 'ash')`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + }, + ], + invalid: [ + { + code: `import 'lodash'`, + filename: path.join(FIXTURE, 'packages/foo/src/index.ts'), + errors: [ + ERR_SWITCHED( + 'lodash', + 'devDependencies', + 'dependencies', + 'packages/foo', + ), + ], + }, + { + code: `import 'react-router'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_SWITCHED( + 'react-router', + 'dependencies', + 'peerDependencies', + 'packages/bar', + ), + ], + }, + { + code: `import 'react-router-dom'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_SWITCHED( + 'react-router-dom', + 'devDependencies', + 'peerDependencies', + 'packages/bar', + ), + ], + }, + { + code: `import 'lodash'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + }, + { + code: `import { debounce } from 'lodash'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + }, + { + code: `import * as _ from 'lodash'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + }, + { + code: `import _ from 'lodash'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + }, + { + code: `import('lodash')`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + }, + { + code: `require('lodash')`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + }, + { + code: `import 'lodash'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + }, + { + code: `import 'lodash'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.test.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'devDependencies', 'packages/bar', '--dev'), + ], + }, + { + code: `import 'react'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('react', 'peerDependencies', 'packages/bar', '--peer'), + ], + }, + { + code: `import 'react'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.test.ts'), + errors: [ + ERR_UNDECLARED('react', 'peerDependencies', 'packages/bar', '--peer'), + ], + }, + { + code: `import 'react-dom'`, + filename: path.join(FIXTURE, 'packages/foo/src/index.ts'), + errors: [ERR_UNDECLARED('react-dom', 'dependencies', 'packages/foo')], + }, + { + code: `import 'react-dom'`, + filename: path.join(FIXTURE, 'packages/foo/src/index.test.ts'), + errors: [ + ERR_UNDECLARED('react-dom', 'devDependencies', 'packages/foo', '--dev'), + ], + }, + ], +}); From 2638dd4e7c9aa88ffcf987a33ff86465292094c4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Feb 2023 11:31:00 +0100 Subject: [PATCH 40/90] eslint-plugin: add tests for no-forbidden-package-imports Signed-off-by: Patrik Oldsberg --- .../monorepo/packages/bar/package.json | 5 + .../monorepo/packages/foo/package.json | 6 +- .../src/no-forbidden-package-imports.test.ts | 120 ++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 packages/eslint-plugin/src/no-forbidden-package-imports.test.ts diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json index c876e00e32..efef27047b 100644 --- a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json @@ -1,5 +1,10 @@ { "name": "@internal/bar", + "exports": { + ".": "./src/index.ts", + "./BarPage": "./src/components/Bar.tsx", + "./package.json": "./package.json" + }, "backstage": { "role": "frontend-plugin" }, diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json index 8a14cad4d3..563ec35120 100644 --- a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json @@ -8,5 +8,9 @@ }, "peerDependencies": { "react": "*" - } + }, + "files": [ + "dist", + "type-utils" + ] } diff --git a/packages/eslint-plugin/src/no-forbidden-package-imports.test.ts b/packages/eslint-plugin/src/no-forbidden-package-imports.test.ts new file mode 100644 index 0000000000..349b71319f --- /dev/null +++ b/packages/eslint-plugin/src/no-forbidden-package-imports.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RuleTester } from 'eslint'; +import path from 'path'; +import rule from '../rules/no-forbidden-package-imports'; + +const RULE = 'no-forbidden-package-imports'; +const FIXTURE = path.resolve(__dirname, '__fixtures__/monorepo'); + +const ERR = (name: string, path: string) => ({ + message: `${name} does not export ${path}`, +}); + +process.chdir(FIXTURE); + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + ecmaVersion: 2021, + }, +}); + +ruleTester.run(RULE, rule, { + valid: [ + { + code: `import '@internal/foo'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/foo/type-utils'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/foo/type-utils/anything'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/foo/package.json'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/bar'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/bar/package.json'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/bar/BarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + ], + invalid: [ + { + code: `import '@internal/foo/FooPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'FooPage')], + }, + { + code: `import '@internal/foo/dist'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'dist')], + }, + { + code: `import '@internal/foo/dist/FooPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'dist/FooPage')], + }, + { + code: `import '@internal/foo/src/FooPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'src/FooPage')], + }, + { + code: `import '@internal/foo/src'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'src')], + }, + { + code: `import '@internal/bar/OtherBarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'OtherBarPage')], + }, + { + code: `import '@internal/bar/dist'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'dist')], + }, + { + code: `import '@internal/bar/dist/BarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'dist/BarPage')], + }, + { + code: `import '@internal/bar/src/BarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'src/BarPage')], + }, + { + code: `import '@internal/bar/src'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'src')], + }, + ], +}); From 02f1316e57d709c7e8e458fa93828ef9ed1e96f2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Feb 2023 13:09:13 +0100 Subject: [PATCH 41/90] changeset: added changesets for eslint plugin fixes that affect the published package Signed-off-by: Patrik Oldsberg --- .changeset/grumpy-fireants-drop.md | 5 +++++ .changeset/wicked-beers-walk.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/grumpy-fireants-drop.md create mode 100644 .changeset/wicked-beers-walk.md diff --git a/.changeset/grumpy-fireants-drop.md b/.changeset/grumpy-fireants-drop.md new file mode 100644 index 0000000000..92b08663f4 --- /dev/null +++ b/.changeset/grumpy-fireants-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/codemods': patch +--- + +Moved `commander` to being a regular dependency. diff --git a/.changeset/wicked-beers-walk.md b/.changeset/wicked-beers-walk.md new file mode 100644 index 0000000000..160a171eb1 --- /dev/null +++ b/.changeset/wicked-beers-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Updated `packages/app/cypress/.eslintrc.json` to remove the unnecessary `import/no-extraneous-dependencies` rule. From ff63acf30a4bc94377b5c4f220deebe07af4d915 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Feb 2023 13:12:16 +0100 Subject: [PATCH 42/90] repo-tools: make api extraction ignore packages without a role Signed-off-by: Patrik Oldsberg --- .changeset/rare-berries-give.md | 5 +++++ .../repo-tools/src/commands/api-reports/api-extractor.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/rare-berries-give.md diff --git a/.changeset/rare-berries-give.md b/.changeset/rare-berries-give.md new file mode 100644 index 0000000000..39e360bf48 --- /dev/null +++ b/.changeset/rare-berries-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Packages without a declared `backstage.role` are now ignored. diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index b037f40be6..39df70c21a 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -1107,7 +1107,7 @@ export async function categorizePackageDirs(packageDirs: any[]) { }); const role = pkgJson?.backstage?.role; if (!role) { - throw new Error(`No backstage.role in ${dir}/package.json`); + return; // Ignore packages without roles } if (role === 'cli') { cliPackageDirs.push(dir); From dec4896baa9182120671195fe1fc81008deacbac Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Feb 2023 13:13:11 +0100 Subject: [PATCH 43/90] cli: add missing @backstage/eslint-plugin dependency Signed-off-by: Patrik Oldsberg --- packages/cli/package.json | 1 + yarn.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 359f16adfa..2f5292e3a0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,6 +34,7 @@ "@backstage/config": "workspace:^", "@backstage/config-loader": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/eslint-plugin": "workspace:^", "@backstage/release-manifests": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", diff --git a/yarn.lock b/yarn.lock index 2d250e9913..069b1125a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3660,6 +3660,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/eslint-plugin": "workspace:^" "@backstage/release-manifests": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" @@ -4094,7 +4095,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/eslint-plugin@workspace:packages/eslint-plugin": +"@backstage/eslint-plugin@workspace:^, @backstage/eslint-plugin@workspace:packages/eslint-plugin": version: 0.0.0-use.local resolution: "@backstage/eslint-plugin@workspace:packages/eslint-plugin" dependencies: From baf6e4c96a90b54a52e8acc28e2dcaf706d63bd4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Feb 2023 13:57:18 +0100 Subject: [PATCH 44/90] backend-test-utils: removed unnecessary @backstage/cli dependency Signed-off-by: Patrik Oldsberg --- .changeset/chilled-sheep-collect.md | 5 +++++ packages/backend-test-utils/package.json | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/chilled-sheep-collect.md diff --git a/.changeset/chilled-sheep-collect.md b/.changeset/chilled-sheep-collect.md new file mode 100644 index 0000000000..b944685e16 --- /dev/null +++ b/.changeset/chilled-sheep-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Removed unnecessary `@backstage/cli` dependency. diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index a01dbaf0fa..d1f0a86d43 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -37,7 +37,6 @@ "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", - "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", From 16acf34bf2b7c14151c91afb96faf4428704e530 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sat, 4 Feb 2023 13:21:37 -0500 Subject: [PATCH 45/90] improve Vault plugin catalog entry language * correct grammar * clarify that the plugin deals with HashiCorp Vault (and not another Vault, such as Ansible Vault) Signed-off-by: Mike Ball --- microsite/data/plugins/vault.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/data/plugins/vault.yaml b/microsite/data/plugins/vault.yaml index 822f5dcc90..bb8f799ba7 100644 --- a/microsite/data/plugins/vault.yaml +++ b/microsite/data/plugins/vault.yaml @@ -3,7 +3,7 @@ title: Vault author: Spread Group authorUrl: https://github.com/ivangonzalezacuna category: Vault -description: Visualize a list secrets stored in your vault instance. +description: Visualize a list of the secrets stored in your HashiCorp Vault instance. documentation: https://github.com/backstage/backstage/tree/master/plugins/vault iconUrl: img/vault.png npmPackageName: '@backstage/plugin-vault' From 9d9cdea1173223a6630e96d93dbc67fdf83bd119 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 30 Jan 2023 15:54:30 +0100 Subject: [PATCH 46/90] create backend-dev-utils with cli ipc client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- .changeset/green-adults-retire.md | 5 + packages/backend-dev-utils/.eslintrc.js | 1 + packages/backend-dev-utils/README.md | 5 + packages/backend-dev-utils/package.json | 36 ++++++ packages/backend-dev-utils/src/index.ts | 21 ++++ packages/backend-dev-utils/src/ipcClient.ts | 120 +++++++++++++++++++ packages/backend-dev-utils/src/setupTests.ts | 16 +++ yarn.lock | 8 ++ 8 files changed, 212 insertions(+) create mode 100644 .changeset/green-adults-retire.md create mode 100644 packages/backend-dev-utils/.eslintrc.js create mode 100644 packages/backend-dev-utils/README.md create mode 100644 packages/backend-dev-utils/package.json create mode 100644 packages/backend-dev-utils/src/index.ts create mode 100644 packages/backend-dev-utils/src/ipcClient.ts create mode 100644 packages/backend-dev-utils/src/setupTests.ts diff --git a/.changeset/green-adults-retire.md b/.changeset/green-adults-retire.md new file mode 100644 index 0000000000..c13e8d664f --- /dev/null +++ b/.changeset/green-adults-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dev-utils': minor +--- + +Introduced a new package for backend development utilities. Similar to how `@backstage/dev-utils` is used in the frontend. diff --git a/packages/backend-dev-utils/.eslintrc.js b/packages/backend-dev-utils/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/backend-dev-utils/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend-dev-utils/README.md b/packages/backend-dev-utils/README.md new file mode 100644 index 0000000000..8acce94cf0 --- /dev/null +++ b/packages/backend-dev-utils/README.md @@ -0,0 +1,5 @@ +# backend-dev-utils + +Welcome to the backend-dev-utils package! + +_This package is experimental._ diff --git a/packages/backend-dev-utils/package.json b/packages/backend-dev-utils/package.json new file mode 100644 index 0000000000..735185e37f --- /dev/null +++ b/packages/backend-dev-utils/package.json @@ -0,0 +1,36 @@ +{ + "name": "@backstage/backend-dev-utils", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend-dev-utils" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/packages/backend-dev-utils/src/index.ts b/packages/backend-dev-utils/src/index.ts new file mode 100644 index 0000000000..bddecad73d --- /dev/null +++ b/packages/backend-dev-utils/src/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Local development helpers for backend. + * + * @packageDocumentation + */ diff --git a/packages/backend-dev-utils/src/ipcClient.ts b/packages/backend-dev-utils/src/ipcClient.ts new file mode 100644 index 0000000000..d3fa184a3f --- /dev/null +++ b/packages/backend-dev-utils/src/ipcClient.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type SendMessage = Exclude; + +interface Request { + id: number; + method: string; + body: unknown; + type: string; +} + +type Response = + | { + type: string; + id: number; + body: unknown; + } + | { + type: string; + id: number; + error: Error; + }; + +const requestType = '@backstage/cli/channel/request'; +const responseType = '@backstage/cli/channel/response'; + +/** + * The client side of an IPC communication channel. + * + * @internal + */ +export class BackstageIpcClient { + #messageId = 0; + #sendMessage: SendMessage; + + /** + * Creates a new client if we're in a child process with IPC and BACKSTAGE_CLI_CHANNEL is set. + */ + static create(): BackstageIpcClient | undefined { + const sendMessage = process.send?.bind(process); + return sendMessage && process.env.BACKSTAGE_CLI_CHANNEL + ? new BackstageIpcClient(sendMessage) + : undefined; + } + + constructor(sendMessage: SendMessage) { + this.#sendMessage = sendMessage; + } + + /** + * Send a request to the parent process and wait for a response. + */ + async request( + method: string, + body: TRequestBody, + ): Promise { + return new Promise((resolve, reject) => { + const id = this.#messageId++; + + const request: Request = { + type: requestType, + id, + method, + body, + }; + + this.#sendMessage(request, (e: Error) => { + if (e) { + reject(e); + return; + } + + const timeout = setTimeout(() => { + reject(new Error('IPC request timed out')); + }, 1000); + timeout.unref(); + + const messageHandler = (response: Response) => { + if (response?.type !== responseType) { + return; + } + if (response.id !== id) { + return; + } + + if ('error' in response) { + const error = new Error(response.error.message); + if (response.error.name) { + error.name = response.error.name; + } + reject(error); + } else { + resolve(response.body as TResponseBody); + } + + clearTimeout(timeout); + process.removeListener('message', messageHandler); + }; + + process.addListener('message', messageHandler as () => void); + }); + }); + } +} + +export const ipcClient = BackstageIpcClient.create(); diff --git a/packages/backend-dev-utils/src/setupTests.ts b/packages/backend-dev-utils/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/packages/backend-dev-utils/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/yarn.lock b/yarn.lock index 3d809a40c8..ecd381c8f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3519,6 +3519,14 @@ __metadata: languageName: unknown linkType: soft +"@backstage/backend-dev-utils@workspace:packages/backend-dev-utils": + version: 0.0.0-use.local + resolution: "@backstage/backend-dev-utils@workspace:packages/backend-dev-utils" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/backend-plugin-api@workspace:^, @backstage/backend-plugin-api@workspace:packages/backend-plugin-api": version: 0.0.0-use.local resolution: "@backstage/backend-plugin-api@workspace:packages/backend-plugin-api" From a70806c3b09b851bc5785cc32700cb5b36f58f39 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 31 Jan 2023 12:08:55 +0100 Subject: [PATCH 47/90] backend-dev-utils: add DevDataStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- packages/backend-dev-utils/api-report.md | 20 +++ .../src/DevDataStore.test.ts | 121 ++++++++++++++++++ .../backend-dev-utils/src/DevDataStore.ts | 99 ++++++++++++++ packages/backend-dev-utils/src/index.ts | 2 + 4 files changed, 242 insertions(+) create mode 100644 packages/backend-dev-utils/api-report.md create mode 100644 packages/backend-dev-utils/src/DevDataStore.test.ts create mode 100644 packages/backend-dev-utils/src/DevDataStore.ts diff --git a/packages/backend-dev-utils/api-report.md b/packages/backend-dev-utils/api-report.md new file mode 100644 index 0000000000..216311ccd8 --- /dev/null +++ b/packages/backend-dev-utils/api-report.md @@ -0,0 +1,20 @@ +## API Report File for "@backstage/backend-dev-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public +export class DevDataStore { + static get(): DevDataStore | undefined; + load(key: string): Promise<{ + loaded: boolean; + data: T; + }>; + save( + key: string, + data: T, + ): Promise<{ + saved: boolean; + }>; +} +``` diff --git a/packages/backend-dev-utils/src/DevDataStore.test.ts b/packages/backend-dev-utils/src/DevDataStore.test.ts new file mode 100644 index 0000000000..2bb3f221ae --- /dev/null +++ b/packages/backend-dev-utils/src/DevDataStore.test.ts @@ -0,0 +1,121 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Serializable, spawn } from 'child_process'; +import { DevDataStore } from './DevDataStore'; +import { BackstageIpcClient } from './ipcClient'; + +function applyIpcTransform(value: Serializable): Promise { + const child = spawn( + 'node', + [ + '--eval', + ` + const listener = msg => { + process.send(msg); + process.removeListener('message', listener); + } + process.addListener('message', listener); + `, + ], + { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + serialization: 'advanced', + }, + ); + + return new Promise(resolve => { + child.addListener('message', resolve); + child.send(value); + }); +} + +class MockIpcClient implements Pick { + #data = new Map(); + + async request(method: string, body: any): Promise { + if (method === 'DevDataStore.save') { + this.#data.set(body.key, body.data); + return { saved: true }; + } else if (method === 'DevDataStore.load') { + if (!this.#data.has(body.key)) { + return { loaded: false, data: undefined }; + } + const data = this.#data.get(body.key)!; + return { loaded: true, data: await applyIpcTransform(data) }; + } + throw new Error('Unknown message'); + } +} + +describe('DevDataStore', () => { + it('should save and load data', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect(store.save('test', { foo: 'bar' })).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: { foo: 'bar' }, + }); + }); + + it('should save and load buffers', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect( + store.save('test', Buffer.from('abc', 'ascii')), + ).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: Buffer.from('abc', 'ascii'), + }); + }); + + it('should save and load array buffers', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect( + store.save('test', new Uint8Array(Buffer.from('abc', 'ascii'))), + ).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: new Uint8Array(Buffer.from('abc', 'ascii')), + }); + }); + + it('should save and load buffers nested in objects', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect( + store.save('test', { + x: Buffer.from('x', 'ascii'), + y: [Buffer.from('y', 'ascii')], + }), + ).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: { x: Buffer.from('x', 'ascii'), y: [Buffer.from('y', 'ascii')] }, + }); + }); +}); diff --git a/packages/backend-dev-utils/src/DevDataStore.ts b/packages/backend-dev-utils/src/DevDataStore.ts new file mode 100644 index 0000000000..4069ab1bdb --- /dev/null +++ b/packages/backend-dev-utils/src/DevDataStore.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BackstageIpcClient, ipcClient } from './ipcClient'; + +interface SaveRequest { + key: string; + data: unknown; +} + +interface SaveResponse { + saved: boolean; +} + +interface LoadRequest { + key: string; +} + +interface LoadResponse { + loaded: boolean; + data: unknown; +} + +/** + * A data store that can be used to store temporary data during development. + * + * @public + */ +export class DevDataStore { + static #instance?: DevDataStore; + + /** + * Tries to acquire a DevDataStore instance. This will only succeed when the backend + * process is being run through the `@backstage/cli` in development mode. + * + * @returns A DevDataStore instance, or undefined if not available. + */ + static get(): DevDataStore | undefined { + if (ipcClient) { + if (!this.#instance) { + this.#instance = new DevDataStore(ipcClient); + } + return this.#instance; + } + return undefined; + } + + /** @internal */ + static forTest(client: Pick): DevDataStore { + return new DevDataStore(client as BackstageIpcClient); + } + + #client: BackstageIpcClient; + + private constructor(client: BackstageIpcClient) { + this.#client = client; + } + + /** + * Save data to the data store. + * + * @param key - The key used to identify the data. + * @param data - The data to save. The data will be serialized using advanced IPC serialization. + * @returns A promise that resolves to a result object that indicates whether the data was saved. + */ + async save(key: string, data: T): Promise<{ saved: boolean }> { + return this.#client.request( + 'DevDataStore.save', + { key, data }, + ); + } + + /** + * Loads data from the data store. + * + * @param key - The key used to identify the data. + * @returns A promise that resolves to a result object that indicates whether the data was loaded, as well as the data. + */ + async load(key: string): Promise<{ loaded: boolean; data: T }> { + const result = await this.#client.request( + 'DevDataStore.load', + { key }, + ); + return result as { loaded: boolean; data: T }; + } +} diff --git a/packages/backend-dev-utils/src/index.ts b/packages/backend-dev-utils/src/index.ts index bddecad73d..9b707db029 100644 --- a/packages/backend-dev-utils/src/index.ts +++ b/packages/backend-dev-utils/src/index.ts @@ -19,3 +19,5 @@ * * @packageDocumentation */ + +export { DevDataStore } from './DevDataStore'; From 90616df9a8784703dabc093337343ceab5d473d0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 31 Jan 2023 16:28:40 +0100 Subject: [PATCH 48/90] cli: add experimental backend start Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .changeset/weak-radios-sing.md | 5 + packages/cli/package.json | 3 + .../cli/src/commands/start/startBackend.ts | 34 +++-- .../cli/src/lib/experimental/ChannelServer.ts | 102 +++++++++++++ .../src/lib/experimental/ServerDataStore.ts | 78 ++++++++++ .../experimental/startBackendExperimental.ts | 138 ++++++++++++++++++ packages/cli/src/types.d.ts | 2 + yarn.lock | 43 +++++- 8 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 .changeset/weak-radios-sing.md create mode 100644 packages/cli/src/lib/experimental/ChannelServer.ts create mode 100644 packages/cli/src/lib/experimental/ServerDataStore.ts create mode 100644 packages/cli/src/lib/experimental/startBackendExperimental.ts diff --git a/.changeset/weak-radios-sing.md b/.changeset/weak-radios-sing.md new file mode 100644 index 0000000000..dff287b565 --- /dev/null +++ b/.changeset/weak-radios-sing.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added an experimental mode for the `package start` command for backend packages. Enabled by setting `EXPERIMENTAL_BACKEND_START`. diff --git a/packages/cli/package.json b/packages/cli/package.json index 909ac8460c..e6b9b7a97e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,6 +36,7 @@ "@backstage/errors": "workspace:^", "@backstage/release-manifests": "workspace:^", "@backstage/types": "workspace:^", + "@esbuild-kit/cjs-loader": "^2.4.1", "@manypkg/get-packages": "^1.1.3", "@octokit/request": "^6.0.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", @@ -65,6 +66,7 @@ "chalk": "^4.0.0", "chokidar": "^3.3.1", "commander": "^9.1.0", + "cross-spawn": "^7.0.3", "css-loader": "^6.5.1", "diff": "^5.0.0", "esbuild": "^0.17.0", @@ -136,6 +138,7 @@ "@backstage/dev-utils": "workspace:^", "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", + "@types/cross-spawn": "^6.0.2", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", diff --git a/packages/cli/src/commands/start/startBackend.ts b/packages/cli/src/commands/start/startBackend.ts index 61ada7a291..3e6a13be96 100644 --- a/packages/cli/src/commands/start/startBackend.ts +++ b/packages/cli/src/commands/start/startBackend.ts @@ -17,6 +17,7 @@ import fs from 'fs-extra'; import { paths } from '../../lib/paths'; import { serveBackend } from '../../lib/bundler'; +import { startBackendExperimental } from '../../lib/experimental/startBackendExperimental'; interface StartBackendOptions { checksEnabled: boolean; @@ -25,17 +26,28 @@ interface StartBackendOptions { } export async function startBackend(options: StartBackendOptions) { - // Cleaning dist/ before we start the dev process helps work around an issue - // where we end up with the entrypoint executing multiple times, causing - // a port bind conflict among other things. - await fs.remove(paths.resolveTarget('dist')); + if (process.env.EXPERIMENTAL_BACKEND_START) { + const waitForExit = await startBackendExperimental({ + entry: 'src/index', + checksEnabled: false, // not supported + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); - const waitForExit = await serveBackend({ - entry: 'src/index', - checksEnabled: options.checksEnabled, - inspectEnabled: options.inspectEnabled, - inspectBrkEnabled: options.inspectBrkEnabled, - }); + await waitForExit(); + } else { + // Cleaning dist/ before we start the dev process helps work around an issue + // where we end up with the entrypoint executing multiple times, causing + // a port bind conflict among other things. + await fs.remove(paths.resolveTarget('dist')); - await waitForExit(); + const waitForExit = await serveBackend({ + entry: 'src/index', + checksEnabled: options.checksEnabled, + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); + + await waitForExit(); + } } diff --git a/packages/cli/src/lib/experimental/ChannelServer.ts b/packages/cli/src/lib/experimental/ChannelServer.ts new file mode 100644 index 0000000000..db949f95b8 --- /dev/null +++ b/packages/cli/src/lib/experimental/ChannelServer.ts @@ -0,0 +1,102 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { serializeError } from '@backstage/errors'; +import { ChildProcess } from 'child_process'; + +interface RequestMeta { + generation: number; +} + +type MethodHandler = ( + req: TRequest, + meta: RequestMeta, +) => Promise; + +interface Request { + id: number; + method: string; + body: unknown; + type: string; +} + +const requestType = '@backstage/cli/channel/request'; +const responseType = '@backstage/cli/channel/response'; + +export class ChannelServer { + #generation = 1; + #methods = new Map>(); + + addChild(child: ChildProcess) { + const generation = this.#generation++; + const sendMessage = child.send?.bind(child); + if (!sendMessage) { + return; + } + + const messageListener = (request: Request) => { + if (request.type !== requestType) { + return; + } + + const handler = this.#methods.get(request.method); + if (!handler) { + sendMessage({ + type: responseType, + id: request.id, + error: { + name: 'NotFoundError', + message: `No handler registered for method ${request.method}`, + }, + }); + return; + } + + Promise.resolve() + .then(() => handler(request.body, { generation })) + .then(response => + sendMessage({ + type: responseType, + id: request.id, + body: response, + }), + ) + .catch(error => + sendMessage({ + type: responseType, + id: request.id, + error: serializeError(error), + }), + ); + }; + + child.addListener('message', messageListener as (req: unknown) => void); + + child.addListener('exit', () => { + child.removeListener('message', messageListener); + }); + } + + registerMethod( + method: string, + handler: MethodHandler, + ) { + if (this.#methods.has(method)) { + throw new Error(`A handler is already registered for method ${method}`); + } + this.#methods.set(method, handler); + } +} diff --git a/packages/cli/src/lib/experimental/ServerDataStore.ts b/packages/cli/src/lib/experimental/ServerDataStore.ts new file mode 100644 index 0000000000..29ac331de5 --- /dev/null +++ b/packages/cli/src/lib/experimental/ServerDataStore.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ChannelServer } from './ChannelServer'; + +interface StorageItem { + generation: number; + data: unknown; +} + +interface SaveRequest { + key: string; + data: unknown; +} + +interface SaveResponse { + saved: boolean; +} + +interface LoadRequest { + key: string; +} + +interface LoadResponse { + loaded: boolean; + data: unknown; +} + +export class ServerDataStore { + static bind(server: ChannelServer): void { + const store = new Map(); + + server.registerMethod( + 'DevDataStore.save', + async (request, { generation }) => { + const { key, data } = request; + if (!key) { + throw new Error('Key is required in DevDataStore.save'); + } + + const item = store.get(key); + + if (!item) { + store.set(key, { generation, data }); + return { saved: true }; + } + + if (item.generation > generation) { + return { saved: false }; + } + + store.set(key, { generation, data }); + return { saved: true }; + }, + ); + + server.registerMethod( + 'DevDataStore.load', + async request => { + const item = store.get(request.key); + return { loaded: Boolean(item), data: item?.data }; + }, + ); + } +} diff --git a/packages/cli/src/lib/experimental/startBackendExperimental.ts b/packages/cli/src/lib/experimental/startBackendExperimental.ts new file mode 100644 index 0000000000..94bf066925 --- /dev/null +++ b/packages/cli/src/lib/experimental/startBackendExperimental.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BackendServeOptions } from '../bundler/types'; +import type { ChildProcess } from 'child_process'; +import { fileURLToPath } from 'url'; +import { isAbsolute as isAbsolutePath } from 'path'; +import { FSWatcher, watch } from 'chokidar'; +import { ChannelServer } from './ChannelServer'; +import { ServerDataStore } from './ServerDataStore'; +import debounce from 'lodash/debounce'; +import spawn from 'cross-spawn'; +import { paths } from '../paths'; + +const loaderArgs = ['--require', require.resolve('@esbuild-kit/cjs-loader')]; + +export async function startBackendExperimental(options: BackendServeOptions) { + const envEnv = process.env as { NODE_ENV: string }; + if (!envEnv.NODE_ENV) { + envEnv.NODE_ENV = 'development'; + } + + // Set up the parent IPC server and bind the available services + const server = new ChannelServer(); + ServerDataStore.bind(server); + + let exiting = false; + let child: ChildProcess | undefined; + let watcher: FSWatcher | undefined = undefined; + let shutdownPromise: Promise | undefined = undefined; + + const restart = debounce(async () => { + // If a re-trigger happens during an existing shutdown, we just ignore it + if (shutdownPromise) { + return; + } + + if (child && !child.killed && child.exitCode === null) { + // We always wait for the existing process to exit, to make sure we don't get IPC conflicts + shutdownPromise = new Promise(resolve => child!.once('exit', resolve)); + child.kill(); + await shutdownPromise; + shutdownPromise = undefined; + } + + // We've received a shutdown signal + if (exiting) { + return; + } + + const optionArgs = new Array(); + if (options.inspectEnabled) { + optionArgs.push('--inspect'); + } else if (options.inspectBrkEnabled) { + optionArgs.push('--inspect-brk'); + } + + const userArgs = process.argv + .slice(['node', 'backstage-cli', 'package', 'start'].length) + .filter(arg => !optionArgs.includes(arg)); + + child = spawn( + process.execPath, + [...loaderArgs, ...optionArgs, options.entry, ...userArgs], + { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + env: { + ...process.env, + BACKSTAGE_CLI_CHANNEL: '1', + ESBK_TSCONFIG_PATH: paths.resolveTargetRoot('tsconfig.json'), + }, + serialization: 'advanced', + }, + ); + + server.addChild(child); + + // This captures messages sent by @esbuild-kit/cjs-loader + child.on('message', (data: { type?: string } | null) => { + if (typeof data === 'object' && data?.type === 'dependency') { + let path = (data as { path: string }).path; + if (path.startsWith('file:')) { + path = fileURLToPath(path); + } + + if (isAbsolutePath(path)) { + watcher?.add(path); + } + } + }); + }, 100); + + restart(); + + watcher = watch([paths.targetDir], { + cwd: process.cwd(), + ignored: ['**/.*/**', '**/node_modules/**'], + ignoreInitial: true, + ignorePermissionErrors: true, + }).on('all', restart); + + // Trigger restart on hitting enter in the terminal + process.stdin.on('data', restart); + + const exitPromise = new Promise(resolveExitPromise => { + async function handleSignal(signal: NodeJS.Signals) { + exiting = true; + + // Forward signals to child and wait for it to exit if still running + if (child && child.exitCode === null) { + await new Promise(resolve => { + child!.on('close', resolve); + child!.kill(signal); + }); + } + + resolveExitPromise(); + } + + process.once('SIGINT', handleSignal); + process.once('SIGTERM', handleSignal); + }); + + return () => exitPromise; +} diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index fedbe61f02..1222b2b4b8 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -258,3 +258,5 @@ declare module 'webpack-node-externals' { } } } + +declare module '@esbuild-kit/cjs-loader' {} diff --git a/yarn.lock b/yarn.lock index ecd381c8f8..7143443674 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3672,6 +3672,7 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" + "@esbuild-kit/cjs-loader": ^2.4.1 "@manypkg/get-packages": ^1.1.3 "@octokit/request": ^6.0.0 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.7 @@ -3690,6 +3691,7 @@ __metadata: "@swc/core": ^1.3.9 "@swc/helpers": ^0.4.7 "@swc/jest": ^0.2.22 + "@types/cross-spawn": ^6.0.2 "@types/diff": ^5.0.0 "@types/express": ^4.17.6 "@types/fs-extra": ^9.0.1 @@ -3717,6 +3719,7 @@ __metadata: chalk: ^4.0.0 chokidar: ^3.3.1 commander: ^9.1.0 + cross-spawn: ^7.0.3 css-loader: ^6.5.1 del: ^6.0.0 diff: ^5.0.0 @@ -9196,6 +9199,26 @@ __metadata: languageName: node linkType: hard +"@esbuild-kit/cjs-loader@npm:^2.4.1": + version: 2.4.1 + resolution: "@esbuild-kit/cjs-loader@npm:2.4.1" + dependencies: + "@esbuild-kit/core-utils": ^3.0.0 + get-tsconfig: ^4.2.0 + checksum: a516065907be0ead76ac2199ccb08ff92659ba5e2edb4bb8772b6a63afe4faed7eb45a3b4d87266a68c7c135c3dba971cd087bc6f16c382356e835c7dd3440f5 + languageName: node + linkType: hard + +"@esbuild-kit/core-utils@npm:^3.0.0": + version: 3.0.0 + resolution: "@esbuild-kit/core-utils@npm:3.0.0" + dependencies: + esbuild: ~0.15.10 + source-map-support: ^0.5.21 + checksum: 0e89ec718e2211bf95c48a8085aaef88e8e416f42abd1c62d488d5458eecd3fbc144179a0c5570ad36fa7e2d3bbc411f8d3fb28802c37ced2154dc2c6ded9dfe + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.17.5": version: 0.17.5 resolution: "@esbuild/android-arm64@npm:0.17.5" @@ -13912,6 +13935,15 @@ __metadata: languageName: node linkType: hard +"@types/cross-spawn@npm:^6.0.2": + version: 6.0.2 + resolution: "@types/cross-spawn@npm:6.0.2" + dependencies: + "@types/node": "*" + checksum: fa9edd32178878cab3ea8d6d0260639e0fe4860ddb3887b8de53d6e8036e154fc5f313c653f690975aa25025aea8beb83fb0870b931bf8d9202c3ac530a24c9d + languageName: node + linkType: hard + "@types/d3-color@npm:*": version: 3.0.2 resolution: "@types/d3-color@npm:3.0.2" @@ -21335,7 +21367,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.15.6": +"esbuild@npm:^0.15.6, esbuild@npm:~0.15.10": version: 0.15.18 resolution: "esbuild@npm:0.15.18" dependencies: @@ -23493,6 +23525,13 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.2.0": + version: 4.3.0 + resolution: "get-tsconfig@npm:4.3.0" + checksum: 2597aab99aa3a24db209e192a3e5874ac47fc5abc71703ee26346e0c5816cb346ca09fc813c739db5862d3a2905d89aeca1b0cbc46c2b272398d672309aaf414 + languageName: node + linkType: hard + "getopts@npm:2.3.0": version: 2.3.0 resolution: "getopts@npm:2.3.0" @@ -35146,7 +35185,7 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.20": +"source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.21, source-map-support@npm:~0.5.20": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" dependencies: From ab2251564746e53a10759267dacdf83bb7d6769d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 31 Jan 2023 17:58:58 +0100 Subject: [PATCH 49/90] backend-app-api: move signal handlers to backend initializer + exit Signed-off-by: Patrik Oldsberg --- .changeset/red-kiwis-compare.md | 5 +++++ .../rootLifecycle/rootLifecycleFactory.ts | 5 +---- .../backend-app-api/src/wiring/BackendInitializer.ts | 12 ++++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 .changeset/red-kiwis-compare.md diff --git a/.changeset/red-kiwis-compare.md b/.changeset/red-kiwis-compare.md new file mode 100644 index 0000000000..6045367105 --- /dev/null +++ b/.changeset/red-kiwis-compare.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +The shutdown signal handlers are now installed as part of the backend instance rather than the lifecycle service, and explicitly cause the process to exit. diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts index 71ab76ae92..ebff53e9a6 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts @@ -22,11 +22,8 @@ import { LoggerService, } from '@backstage/backend-plugin-api'; -const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit']; export class BackendLifecycleImpl implements RootLifecycleService { - constructor(private readonly logger: LoggerService) { - CALLBACKS.map(signal => process.on(signal, () => this.shutdown())); - } + constructor(private readonly logger: LoggerService) {} #isCalled = false; #shutdownTasks: Array = []; diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index ce17398a79..d1139615f1 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -87,6 +87,18 @@ export class BackendInitializer { } this.#started = true; + for (const event of ['SIGTERM', 'SIGINT', 'beforeExit']) { + process.on(event, async () => { + try { + await this.stop(); + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } + }); + } + // Initialize all root scoped services for (const ref of this.#serviceHolder.getServiceRefs()) { if (ref.scope === 'root') { From f60cca9da1defe5786a0f16bb17fac2502fca043 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Feb 2023 10:57:42 +0100 Subject: [PATCH 50/90] backend-common: add support for restoring database state via dev store Signed-off-by: Patrik Oldsberg --- .changeset/long-chefs-burn.md | 5 ++ .changeset/old-ways-know.md | 5 ++ .../database/databaseFactory.ts | 10 +++- packages/backend-common/api-report.md | 14 ++++- packages/backend-common/package.json | 1 + .../src/database/DatabaseManager.ts | 15 +++-- .../backend-common/src/database/connection.ts | 5 +- .../src/database/connectors/sqlite3.ts | 55 +++++++++++++++---- packages/backend-common/src/database/index.ts | 5 +- packages/backend-common/src/database/types.ts | 20 ++++++- yarn.lock | 3 +- 11 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 .changeset/long-chefs-burn.md create mode 100644 .changeset/old-ways-know.md diff --git a/.changeset/long-chefs-burn.md b/.changeset/long-chefs-burn.md new file mode 100644 index 0000000000..03a7b76db9 --- /dev/null +++ b/.changeset/long-chefs-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Updated database factory to pass service deps required for restoring database state during development. diff --git a/.changeset/old-ways-know.md b/.changeset/old-ways-know.md new file mode 100644 index 0000000000..60a1a923e8 --- /dev/null +++ b/.changeset/old-ways-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +The `DatabaseManager.forPlugin` method now accepts additional service dependencies. There is no need to update existing code to pass these dependencies. diff --git a/packages/backend-app-api/src/services/implementations/database/databaseFactory.ts b/packages/backend-app-api/src/services/implementations/database/databaseFactory.ts index 8baf84cb69..4d95f863db 100644 --- a/packages/backend-app-api/src/services/implementations/database/databaseFactory.ts +++ b/packages/backend-app-api/src/services/implementations/database/databaseFactory.ts @@ -26,7 +26,8 @@ export const databaseFactory = createServiceFactory({ service: coreServices.database, deps: { config: coreServices.config, - plugin: coreServices.pluginMetadata, + lifecycle: coreServices.lifecycle, + pluginMetadata: coreServices.pluginMetadata, }, async createRootContext({ config }) { return config.getOptional('backend.database') @@ -39,7 +40,10 @@ export const databaseFactory = createServiceFactory({ }), ); }, - async factory({ plugin }, databaseManager) { - return databaseManager.forPlugin(plugin.getId()); + async factory({ pluginMetadata, lifecycle }, databaseManager) { + return databaseManager.forPlugin(pluginMetadata.getId(), { + pluginMetadata, + lifecycle, + }); }, }); diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index b03d6ec866..8b6311f8b0 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -32,6 +32,7 @@ import { IdentityService } from '@backstage/backend-plugin-api'; import { isChildPath } from '@backstage/cli-common'; import { Knex } from 'knex'; import { KubeConfig } from '@kubernetes/client-node'; +import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoadConfigOptionsRemote } from '@backstage/config-loader'; import { Logger } from 'winston'; import { LoggerService } from '@backstage/backend-plugin-api'; @@ -40,6 +41,7 @@ import { PermissionsService } from '@backstage/backend-plugin-api'; import { CacheService as PluginCacheManager } from '@backstage/backend-plugin-api'; import { DatabaseService as PluginDatabaseManager } from '@backstage/backend-plugin-api'; import { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api'; +import { PluginMetadataService } from '@backstage/backend-plugin-api'; import { PushResult } from 'isomorphic-git'; import { Readable } from 'stream'; import { ReadCommitResult } from 'isomorphic-git'; @@ -232,6 +234,7 @@ export class Contexts { export function createDatabaseClient( dbConfig: Config, overrides?: Partial, + deps?: PluginDatabaseDependencies, ): Knex; // @public @@ -252,7 +255,10 @@ export function createStatusCheckRouter(options: { // @public export class DatabaseManager { - forPlugin(pluginId: string): PluginDatabaseManager; + forPlugin( + pluginId: string, + deps?: PluginDatabaseDependencies, + ): PluginDatabaseManager; static fromConfig( config: Config, options?: DatabaseManagerOptions, @@ -571,6 +577,12 @@ export function notFoundHandler(): RequestHandler; export { PluginCacheManager }; +// @public +export type PluginDatabaseDependencies = { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; +}; + export { PluginDatabaseManager }; export { PluginEndpointDiscovery }; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index c950950680..9f1069f322 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@backstage/backend-app-api": "workspace:^", + "@backstage/backend-dev-utils": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/config": "workspace:^", diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 5a497fff29..d8205faec1 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -27,7 +27,7 @@ import { ensureSchemaExists, normalizeConnection, } from './connection'; -import { PluginDatabaseManager } from './types'; +import { PluginDatabaseDependencies, PluginDatabaseManager } from './types'; import path from 'path'; import { LoggerService } from '@backstage/backend-plugin-api'; import { stringifyError } from '@backstage/errors'; @@ -94,12 +94,15 @@ export class DatabaseManager { * should be unique as they are used to look up database config overrides under * `backend.database.plugin`. */ - forPlugin(pluginId: string): PluginDatabaseManager { + forPlugin( + pluginId: string, + deps?: PluginDatabaseDependencies, + ): PluginDatabaseManager { const _this = this; return { getClient(): Promise { - return _this.getDatabase(pluginId); + return _this.getDatabase(pluginId, deps); }, migrations: { skip: false, @@ -307,7 +310,10 @@ export class DatabaseManager { * @returns Promise which resolves to a scoped Knex database client for a * plugin */ - private async getDatabase(pluginId: string): Promise { + private async getDatabase( + pluginId: string, + deps?: PluginDatabaseDependencies, + ): Promise { if (this.databaseCache.has(pluginId)) { return this.databaseCache.get(pluginId)!; } @@ -351,6 +357,7 @@ export class DatabaseManager { const client = createDatabaseClient( pluginConfig, databaseClientOverrides, + deps, ); this.startKeepaliveLoop(pluginId, client); return client; diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 5f487886b4..054ab7cfa8 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -19,7 +19,7 @@ import { JsonObject } from '@backstage/types'; import { InputError } from '@backstage/errors'; import knexFactory, { Knex } from 'knex'; import { mergeDatabaseConfig } from './config'; -import { DatabaseConnector } from './types'; +import { DatabaseConnector, PluginDatabaseDependencies } from './types'; import { mysqlConnector, pgConnector, sqlite3Connector } from './connectors'; @@ -55,11 +55,12 @@ const ConnectorMapping: Record = { export function createDatabaseClient( dbConfig: Config, overrides?: Partial, + deps?: PluginDatabaseDependencies, ) { const client: DatabaseClient = dbConfig.getString('client'); return ( - ConnectorMapping[client]?.createClient(dbConfig, overrides) ?? + ConnectorMapping[client]?.createClient(dbConfig, overrides, deps) ?? knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides)) ); } diff --git a/packages/backend-common/src/database/connectors/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts index 6426005f0e..299e18a111 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -18,8 +18,9 @@ import { Config } from '@backstage/config'; import { ensureDirSync } from 'fs-extra'; import knexFactory, { Knex } from 'knex'; import path from 'path'; +import { DevDataStore } from '@backstage/backend-dev-utils'; import { mergeDatabaseConfig } from '../config'; -import { DatabaseConnector } from '../types'; +import { DatabaseConnector, PluginDatabaseDependencies } from '../types'; /** * Creates a knex SQLite3 database connection @@ -30,22 +31,56 @@ import { DatabaseConnector } from '../types'; export function createSqliteDatabaseClient( dbConfig: Config, overrides?: Knex.Config, + deps?: PluginDatabaseDependencies, ) { const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides); + const connConfig = knexConfig.connection as Knex.Sqlite3ConnectionConfig; // If storage on disk is used, ensure that the directory exists - if ( - (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename && - (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename !== - ':memory:' - ) { - const { filename } = knexConfig.connection as Knex.Sqlite3ConnectionConfig; - const directory = path.dirname(filename); - + if (connConfig.filename && connConfig.filename !== ':memory:') { + const directory = path.dirname(connConfig.filename); ensureDirSync(directory); } - const database = knexFactory(knexConfig); + let database: Knex; + + if (deps && connConfig.filename === ':memory:') { + // The dev store is used during watch mode to store and restore the database + // across reloads. It is only available when running the backend through + // `backstage-cli package start`. + const devStore = DevDataStore.get(); + const dataKey = `sqlite3-db-${deps.pluginMetadata.getId()}`; + + if (devStore) { + database = knexFactory({ + ...knexConfig, + connection: async () => { + // If seed data is available, use it to restore the database + const { data: seedData } = await devStore?.load(dataKey); + + return { + ...(knexConfig.connection as Knex.Sqlite3ConnectionConfig), + filename: seedData ?? ':memory:', + }; + }, + }); + } else { + database = knexFactory(knexConfig); + } + + if (devStore) { + // If the dev store is available we save the database state on shutdown + deps?.lifecycle?.addShutdownHook({ + async fn() { + const connection = await database.client.acquireConnection(); + const data = connection.serialize(); + await devStore.save(dataKey, data); + }, + }); + } + } else { + database = knexFactory(knexConfig); + } database.client.pool.on('createSuccess', (_eventId: any, resource: any) => { resource.run('PRAGMA foreign_keys = ON', () => {}); diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 0dde1ec239..483dbd9e40 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -22,5 +22,8 @@ export * from './DatabaseManager'; */ export { createDatabaseClient, ensureDatabaseExists } from './connection'; -export type { PluginDatabaseManager } from './types'; +export type { + PluginDatabaseManager, + PluginDatabaseDependencies, +} from './types'; export { isDatabaseConflictError } from './util'; diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index 2d59cf636e..7e96c44071 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -14,11 +14,25 @@ * limitations under the License. */ +import { + LifecycleService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { Knex } from 'knex'; export type { DatabaseService as PluginDatabaseManager } from '@backstage/backend-plugin-api'; +/** + * Service dependencies for `PluginDatabaseManager`. + * + * @public + */ +export type PluginDatabaseDependencies = { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; +}; + /** * DatabaseConnector manages an underlying Knex database driver. */ @@ -26,7 +40,11 @@ export interface DatabaseConnector { /** * createClient provides an instance of a knex database connector. */ - createClient(dbConfig: Config, overrides?: Partial): Knex; + createClient( + dbConfig: Config, + overrides?: Partial, + deps?: PluginDatabaseDependencies, + ): Knex; /** * createNameOverride provides a partial knex config sufficient to override a * database name. diff --git a/yarn.lock b/yarn.lock index 7143443674..650cb4c95d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3423,6 +3423,7 @@ __metadata: resolution: "@backstage/backend-common@workspace:packages/backend-common" dependencies: "@backstage/backend-app-api": "workspace:^" + "@backstage/backend-dev-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -3519,7 +3520,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/backend-dev-utils@workspace:packages/backend-dev-utils": +"@backstage/backend-dev-utils@workspace:^, @backstage/backend-dev-utils@workspace:packages/backend-dev-utils": version: 0.0.0-use.local resolution: "@backstage/backend-dev-utils@workspace:packages/backend-dev-utils" dependencies: From cd4d0365ac9feddd40abe820d3c9097f92575202 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Feb 2023 11:08:32 +0100 Subject: [PATCH 51/90] backend-next: use experimental start script Signed-off-by: Patrik Oldsberg --- packages/backend-next/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index cc537ef514..fb821e19eb 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -18,7 +18,7 @@ "backstage" ], "scripts": { - "start": "backstage-cli package start", + "start": "EXPERIMENTAL_BACKEND_START=1 backstage-cli package start", "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", From 03570c71e44b81f0da89d7cf244c79e4687e6046 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Feb 2023 16:07:08 +0100 Subject: [PATCH 52/90] backend-common: inline plugin database deps type Signed-off-by: Patrik Oldsberg --- packages/backend-common/api-report.md | 16 ++++++++-------- .../src/database/DatabaseManager.ts | 18 ++++++++++++++---- .../backend-common/src/database/connection.ts | 11 +++++++++-- .../src/database/connectors/sqlite3.ts | 11 +++++++++-- packages/backend-common/src/database/index.ts | 5 +---- packages/backend-common/src/database/types.ts | 15 ++++----------- 6 files changed, 45 insertions(+), 31 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 8b6311f8b0..e77a58b34c 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -234,7 +234,10 @@ export class Contexts { export function createDatabaseClient( dbConfig: Config, overrides?: Partial, - deps?: PluginDatabaseDependencies, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ): Knex; // @public @@ -257,7 +260,10 @@ export function createStatusCheckRouter(options: { export class DatabaseManager { forPlugin( pluginId: string, - deps?: PluginDatabaseDependencies, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ): PluginDatabaseManager; static fromConfig( config: Config, @@ -577,12 +583,6 @@ export function notFoundHandler(): RequestHandler; export { PluginCacheManager }; -// @public -export type PluginDatabaseDependencies = { - lifecycle: LifecycleService; - pluginMetadata: PluginMetadataService; -}; - export { PluginDatabaseManager }; export { PluginEndpointDiscovery }; diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index d8205faec1..08979a92f8 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -27,9 +27,13 @@ import { ensureSchemaExists, normalizeConnection, } from './connection'; -import { PluginDatabaseDependencies, PluginDatabaseManager } from './types'; +import { PluginDatabaseManager } from './types'; import path from 'path'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { + LifecycleService, + LoggerService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; import { stringifyError } from '@backstage/errors'; /** @@ -96,7 +100,10 @@ export class DatabaseManager { */ forPlugin( pluginId: string, - deps?: PluginDatabaseDependencies, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ): PluginDatabaseManager { const _this = this; @@ -312,7 +319,10 @@ export class DatabaseManager { */ private async getDatabase( pluginId: string, - deps?: PluginDatabaseDependencies, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ): Promise { if (this.databaseCache.has(pluginId)) { return this.databaseCache.get(pluginId)!; diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 054ab7cfa8..ff10d349dd 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -19,9 +19,13 @@ import { JsonObject } from '@backstage/types'; import { InputError } from '@backstage/errors'; import knexFactory, { Knex } from 'knex'; import { mergeDatabaseConfig } from './config'; -import { DatabaseConnector, PluginDatabaseDependencies } from './types'; +import { DatabaseConnector } from './types'; import { mysqlConnector, pgConnector, sqlite3Connector } from './connectors'; +import { + LifecycleService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; type DatabaseClient = | 'pg' @@ -55,7 +59,10 @@ const ConnectorMapping: Record = { export function createDatabaseClient( dbConfig: Config, overrides?: Partial, - deps?: PluginDatabaseDependencies, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ) { const client: DatabaseClient = dbConfig.getString('client'); diff --git a/packages/backend-common/src/database/connectors/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts index 299e18a111..5a2dc46f18 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -20,7 +20,11 @@ import knexFactory, { Knex } from 'knex'; import path from 'path'; import { DevDataStore } from '@backstage/backend-dev-utils'; import { mergeDatabaseConfig } from '../config'; -import { DatabaseConnector, PluginDatabaseDependencies } from '../types'; +import { DatabaseConnector } from '../types'; +import { + LifecycleService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; /** * Creates a knex SQLite3 database connection @@ -31,7 +35,10 @@ import { DatabaseConnector, PluginDatabaseDependencies } from '../types'; export function createSqliteDatabaseClient( dbConfig: Config, overrides?: Knex.Config, - deps?: PluginDatabaseDependencies, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ) { const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides); const connConfig = knexConfig.connection as Knex.Sqlite3ConnectionConfig; diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 483dbd9e40..0dde1ec239 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -22,8 +22,5 @@ export * from './DatabaseManager'; */ export { createDatabaseClient, ensureDatabaseExists } from './connection'; -export type { - PluginDatabaseManager, - PluginDatabaseDependencies, -} from './types'; +export type { PluginDatabaseManager } from './types'; export { isDatabaseConflictError } from './util'; diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index 7e96c44071..621bcc676b 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -23,16 +23,6 @@ import { Knex } from 'knex'; export type { DatabaseService as PluginDatabaseManager } from '@backstage/backend-plugin-api'; -/** - * Service dependencies for `PluginDatabaseManager`. - * - * @public - */ -export type PluginDatabaseDependencies = { - lifecycle: LifecycleService; - pluginMetadata: PluginMetadataService; -}; - /** * DatabaseConnector manages an underlying Knex database driver. */ @@ -43,7 +33,10 @@ export interface DatabaseConnector { createClient( dbConfig: Config, overrides?: Partial, - deps?: PluginDatabaseDependencies, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ): Knex; /** * createNameOverride provides a partial knex config sufficient to override a From cbeba4b6f38d7db41f5b9d1f90585af2debaf614 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Feb 2023 16:07:49 +0100 Subject: [PATCH 53/90] backend-common: simplify SQLite setup + work around warning Signed-off-by: Patrik Oldsberg --- .../src/database/connectors/sqlite3.ts | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/backend-common/src/database/connectors/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts index 5a2dc46f18..e1ed6b18e9 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -43,47 +43,53 @@ export function createSqliteDatabaseClient( const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides); const connConfig = knexConfig.connection as Knex.Sqlite3ConnectionConfig; + const filename = connConfig.filename ?? ':memory:'; + // If storage on disk is used, ensure that the directory exists - if (connConfig.filename && connConfig.filename !== ':memory:') { - const directory = path.dirname(connConfig.filename); + if (filename !== ':memory:') { + const directory = path.dirname(filename); ensureDirSync(directory); } let database: Knex; - if (deps && connConfig.filename === ':memory:') { + if (deps && filename === ':memory:') { // The dev store is used during watch mode to store and restore the database // across reloads. It is only available when running the backend through // `backstage-cli package start`. const devStore = DevDataStore.get(); - const dataKey = `sqlite3-db-${deps.pluginMetadata.getId()}`; if (devStore) { + const dataKey = `sqlite3-db-${deps.pluginMetadata.getId()}`; + + const connectionLoader = async () => { + // If seed data is available, use it tconnectionLoader restore the database + const { data: seedData } = await devStore.load(dataKey); + + return { + ...(knexConfig.connection as Knex.Sqlite3ConnectionConfig), + filename: seedData ?? ':memory:', + }; + }; + database = knexFactory({ ...knexConfig, - connection: async () => { - // If seed data is available, use it to restore the database - const { data: seedData } = await devStore?.load(dataKey); - - return { - ...(knexConfig.connection as Knex.Sqlite3ConnectionConfig), - filename: seedData ?? ':memory:', - }; - }, + connection: Object.assign(connectionLoader, { + // This is a workaround for the knex SQLite driver always warning when using a config loader + filename: ':memory:', + }), }); - } else { - database = knexFactory(knexConfig); - } - if (devStore) { // If the dev store is available we save the database state on shutdown - deps?.lifecycle?.addShutdownHook({ + deps.lifecycle.addShutdownHook({ async fn() { const connection = await database.client.acquireConnection(); const data = connection.serialize(); await devStore.save(dataKey, data); }, }); + } else { + database = knexFactory(knexConfig); } } else { database = knexFactory(knexConfig); From 4f5bf2835c43699d61d6313abde4ec9a2329d025 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Feb 2023 16:11:23 +0100 Subject: [PATCH 54/90] backend-app-api: tweak backend initializer to wait for startup before shutdown Signed-off-by: Patrik Oldsberg --- .../src/wiring/BackendInitializer.ts | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index d1139615f1..4bdee8b085 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -28,7 +28,7 @@ import { } from './types'; export class BackendInitializer { - #started = false; + #startPromise?: Promise; #features = new Map(); #registerInits = new Array(); #extensionPoints = new Map, unknown>(); @@ -75,30 +75,40 @@ export class BackendInitializer { } add(feature: BackendFeature, options?: TOptions) { - if (this.#started) { + if (this.#startPromise) { throw new Error('feature can not be added after the backend has started'); } this.#features.set(feature, options); } async start(): Promise { - if (this.#started) { + if (this.#startPromise) { throw new Error('Backend has already started'); } - this.#started = true; - for (const event of ['SIGTERM', 'SIGINT', 'beforeExit']) { - process.on(event, async () => { - try { - await this.stop(); - process.exit(0); - } catch (error) { - console.error(error); - process.exit(1); - } - }); - } + const exitHandler = async () => { + process.removeListener('SIGTERM', exitHandler); + process.removeListener('SIGINT', exitHandler); + process.removeListener('beforeExit', exitHandler); + try { + await this.stop(); + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } + }; + + process.addListener('SIGTERM', exitHandler); + process.addListener('SIGINT', exitHandler); + process.addListener('beforeExit', exitHandler); + + this.#startPromise = this.#doStart(); + await this.#startPromise; + } + + async #doStart(): Promise { // Initialize all root scoped services for (const ref of this.#serviceHolder.getServiceRefs()) { if (ref.scope === 'root') { @@ -189,9 +199,10 @@ export class BackendInitializer { } async stop(): Promise { - if (!this.#started) { + if (!this.#startPromise) { return; } + await this.#startPromise; const lifecycleService = await this.#serviceHolder.get( coreServices.rootLifecycle, From 81c06d5a4ad0e4579e40526dcb73e5f7728b21a3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Feb 2023 16:11:47 +0100 Subject: [PATCH 55/90] backend-dev-utils: remove message listener when IPC client reuqest times out Signed-off-by: Patrik Oldsberg --- packages/backend-dev-utils/src/ipcClient.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/backend-dev-utils/src/ipcClient.ts b/packages/backend-dev-utils/src/ipcClient.ts index d3fa184a3f..87c1596a39 100644 --- a/packages/backend-dev-utils/src/ipcClient.ts +++ b/packages/backend-dev-utils/src/ipcClient.ts @@ -84,10 +84,7 @@ export class BackstageIpcClient { return; } - const timeout = setTimeout(() => { - reject(new Error('IPC request timed out')); - }, 1000); - timeout.unref(); + let timeout: NodeJS.Timeout | undefined = undefined; const messageHandler = (response: Response) => { if (response?.type !== responseType) { @@ -111,6 +108,12 @@ export class BackstageIpcClient { process.removeListener('message', messageHandler); }; + timeout = setTimeout(() => { + reject(new Error(`IPC request '${method}' with ID ${id} timed out`)); + process.removeListener('message', messageHandler); + }, 5000); + timeout.unref(); + process.addListener('message', messageHandler as () => void); }); }); From 1e6f5fcfede627b980d8e627b94ed8b857219213 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Feb 2023 16:12:53 +0100 Subject: [PATCH 56/90] cli: rename ChannelServer to IpcServer Signed-off-by: Patrik Oldsberg --- .../src/lib/experimental/{ChannelServer.ts => IpcServer.ts} | 2 +- packages/cli/src/lib/experimental/ServerDataStore.ts | 4 ++-- packages/cli/src/lib/experimental/startBackendExperimental.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/cli/src/lib/experimental/{ChannelServer.ts => IpcServer.ts} (98%) diff --git a/packages/cli/src/lib/experimental/ChannelServer.ts b/packages/cli/src/lib/experimental/IpcServer.ts similarity index 98% rename from packages/cli/src/lib/experimental/ChannelServer.ts rename to packages/cli/src/lib/experimental/IpcServer.ts index db949f95b8..8ceb72b046 100644 --- a/packages/cli/src/lib/experimental/ChannelServer.ts +++ b/packages/cli/src/lib/experimental/IpcServer.ts @@ -36,7 +36,7 @@ interface Request { const requestType = '@backstage/cli/channel/request'; const responseType = '@backstage/cli/channel/response'; -export class ChannelServer { +export class IpcServer { #generation = 1; #methods = new Map>(); diff --git a/packages/cli/src/lib/experimental/ServerDataStore.ts b/packages/cli/src/lib/experimental/ServerDataStore.ts index 29ac331de5..c0232157cb 100644 --- a/packages/cli/src/lib/experimental/ServerDataStore.ts +++ b/packages/cli/src/lib/experimental/ServerDataStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ChannelServer } from './ChannelServer'; +import { IpcServer } from './IpcServer'; interface StorageItem { generation: number; @@ -40,7 +40,7 @@ interface LoadResponse { } export class ServerDataStore { - static bind(server: ChannelServer): void { + static bind(server: IpcServer): void { const store = new Map(); server.registerMethod( diff --git a/packages/cli/src/lib/experimental/startBackendExperimental.ts b/packages/cli/src/lib/experimental/startBackendExperimental.ts index 94bf066925..c08fbda0da 100644 --- a/packages/cli/src/lib/experimental/startBackendExperimental.ts +++ b/packages/cli/src/lib/experimental/startBackendExperimental.ts @@ -19,7 +19,7 @@ import type { ChildProcess } from 'child_process'; import { fileURLToPath } from 'url'; import { isAbsolute as isAbsolutePath } from 'path'; import { FSWatcher, watch } from 'chokidar'; -import { ChannelServer } from './ChannelServer'; +import { IpcServer } from './IpcServer'; import { ServerDataStore } from './ServerDataStore'; import debounce from 'lodash/debounce'; import spawn from 'cross-spawn'; @@ -34,7 +34,7 @@ export async function startBackendExperimental(options: BackendServeOptions) { } // Set up the parent IPC server and bind the available services - const server = new ChannelServer(); + const server = new IpcServer(); ServerDataStore.bind(server); let exiting = false; From 04f8ef30a6804d9e8048b8d7e591c3b644d40889 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Mon, 6 Feb 2023 12:01:47 +0100 Subject: [PATCH 57/90] Update slow-moles-sin.md Signed-off-by: Ben Lambert --- .changeset/slow-moles-sin.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/slow-moles-sin.md b/.changeset/slow-moles-sin.md index 13527b509e..140d9e7435 100644 --- a/.changeset/slow-moles-sin.md +++ b/.changeset/slow-moles-sin.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-circleci': minor +'@backstage/plugin-circleci': patch --- -Making workflow a link +Making workflow a link From acf6c8c10b2c9f5206456f3216280019a00e3df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C5=A0neberger?= Date: Mon, 6 Feb 2023 13:49:48 +0100 Subject: [PATCH 58/90] [TechRadar] Deprecate RadarEntry.url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marek Šneberger --- .changeset/hip-chairs-double.md | 5 +++++ .changeset/slow-moles-sin.md | 2 +- plugins/tech-radar/src/api.ts | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/hip-chairs-double.md diff --git a/.changeset/hip-chairs-double.md b/.changeset/hip-chairs-double.md new file mode 100644 index 0000000000..73e939ba21 --- /dev/null +++ b/.changeset/hip-chairs-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': minor +--- + +Deprecate `RadarEntry.url` - use `RadarEntry.links` instead diff --git a/.changeset/slow-moles-sin.md b/.changeset/slow-moles-sin.md index 140d9e7435..02e1952312 100644 --- a/.changeset/slow-moles-sin.md +++ b/.changeset/slow-moles-sin.md @@ -2,4 +2,4 @@ '@backstage/plugin-circleci': patch --- -Making workflow a link +Making workflow a link diff --git a/plugins/tech-radar/src/api.ts b/plugins/tech-radar/src/api.ts index 0fbc88255a..bf90cf0750 100644 --- a/plugins/tech-radar/src/api.ts +++ b/plugins/tech-radar/src/api.ts @@ -133,6 +133,8 @@ export interface RadarEntry { * @remarks * * You can use `#` if you don't want to provide any other url + * + * @deprecated Use {@link RadarEntry.links} instead */ url: string; /** From 3c5c6807fb81ad7aec78f097d92e71834ce88a19 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Mon, 6 Feb 2023 07:55:44 -0500 Subject: [PATCH 59/90] Remove title so it doesn't show up twice Signed-off-by: Taras Mankovski --- microsite/blog/2023-01-31-incremental-entity-provider.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/microsite/blog/2023-01-31-incremental-entity-provider.md b/microsite/blog/2023-01-31-incremental-entity-provider.md index ff5f346ccd..c14c3fc15d 100644 --- a/microsite/blog/2023-01-31-incremental-entity-provider.md +++ b/microsite/blog/2023-01-31-incremental-entity-provider.md @@ -4,8 +4,6 @@ author: Paul Cowan & Taras Mankovski authorURL: https://frontside.com/ --- -# Scaling Backstage Ingestion with Incremental Entity Providers - At the heart of [Backstage](https://backstage.io/) is the [Backstage Software Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview), which is a data store that allows an organization to centralize and visualize its many software services and components. Backstage inspects and transforms an organization's disparate software services and parts into a centralized data store. ![catalog pipeline](assets/2023-01-31/catalog-pipeline.png) From 9a58b4127e6a4a5fad3000a84aa71135fa40ca13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C5=A0neberger?= Date: Mon, 6 Feb 2023 14:17:47 +0100 Subject: [PATCH 60/90] Build API report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marek Šneberger --- plugins/tech-radar/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/tech-radar/api-report.md b/plugins/tech-radar/api-report.md index e2744872d8..2836e04461 100644 --- a/plugins/tech-radar/api-report.md +++ b/plugins/tech-radar/api-report.md @@ -25,6 +25,7 @@ export interface RadarEntry { quadrant: string; timeline: Array; title: string; + // @deprecated url: string; } From 28f9883440f254856b08d35812772295262f527a Mon Sep 17 00:00:00 2001 From: LauraMoraB Date: Mon, 6 Feb 2023 14:55:39 +0100 Subject: [PATCH 61/90] fix(catalog/msgraph): read user.select from config Signed-off-by: LauraMoraB --- .changeset/silly-buckets-joke.md | 5 +++++ .../src/microsoftGraph/config.test.ts | 4 ++++ .../src/microsoftGraph/config.ts | 2 ++ 3 files changed, 11 insertions(+) create mode 100644 .changeset/silly-buckets-joke.md diff --git a/.changeset/silly-buckets-joke.md b/.changeset/silly-buckets-joke.md new file mode 100644 index 0000000000..bd77f2ed05 --- /dev/null +++ b/.changeset/silly-buckets-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Fixed bug in readProviderConfig as it wasn't reading the user.select field expected from the app-config.yaml configuration diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index 68c174665d..b02894dfd9 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -52,6 +52,7 @@ describe('readMicrosoftGraphConfig', () => { authority: 'https://login.example.com/', userExpand: 'manager', userFilter: 'accountEnabled eq true', + userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], groupFilter: 'securityEnabled eq false', @@ -69,6 +70,7 @@ describe('readMicrosoftGraphConfig', () => { authority: 'https://login.example.com/', userExpand: 'manager', userFilter: 'accountEnabled eq true', + userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], groupFilter: 'securityEnabled eq false', @@ -167,6 +169,7 @@ describe('readProviderConfigs', () => { user: { expand: 'manager', filter: 'accountEnabled eq true', + select: ['id', 'displayName', 'department'], }, group: { expand: 'member', @@ -196,6 +199,7 @@ describe('readProviderConfigs', () => { queryMode: 'advanced', userExpand: 'manager', userFilter: 'accountEnabled eq true', + userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], groupFilter: 'securityEnabled eq false', diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index 506951ff10..cf4ec646a2 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -264,6 +264,7 @@ export function readProviderConfig( const userExpand = config.getOptionalString('user.expand'); const userFilter = config.getOptionalString('user.filter'); + const userSelect = config.getOptionalStringArray('user.select'); const groupExpand = config.getOptionalString('group.expand'); const groupFilter = config.getOptionalString('group.filter'); @@ -318,6 +319,7 @@ export function readProviderConfig( tenantId, userExpand, userFilter, + userSelect, groupExpand, groupFilter, groupSearch, From 4382e0c7a2ce78db27d668d936f5c3aae41d286d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C5=A0neberger?= Date: Mon, 6 Feb 2023 15:24:33 +0100 Subject: [PATCH 62/90] Remove deprecated url from `sample.ts` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marek Šneberger --- plugins/tech-radar/src/sample.ts | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/plugins/tech-radar/src/sample.ts b/plugins/tech-radar/src/sample.ts index 1c4f7a1a01..0dc647ac54 100644 --- a/plugins/tech-radar/src/sample.ts +++ b/plugins/tech-radar/src/sample.ts @@ -45,12 +45,16 @@ entries.push({ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', }, ], - url: 'https://www.javascript.com/', + url: '#', key: 'javascript', id: 'javascript', title: 'JavaScript', quadrant: 'languages', links: [ + { + url: 'https://www.javascript.com/', + title: 'Learn more', + }, { url: 'https://www.typescriptlang.org/', title: 'TypeScript', @@ -87,7 +91,13 @@ entries.push({ 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur', }, ], - url: 'https://webpack.js.org/', + url: '#', + links: [ + { + url: 'https://webpack.js.org/', + title: 'Learn more', + }, + ], key: 'webpack', id: 'webpack', title: 'Webpack', @@ -101,7 +111,13 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: 'https://reactjs.org/', + url: '#', + links: [ + { + url: 'https://reactjs.org/', + title: 'Learn more', + }, + ], key: 'react', id: 'react', title: 'React', @@ -170,7 +186,13 @@ entries.push({ description: 'long description', }, ], - url: 'https://github.com', + url: '#', + links: [ + { + url: 'https://reactjs.org/', + title: 'Learn more', + }, + ], key: 'github-actions', id: 'github-actions', title: 'GitHub Actions', From 8179f9df3eb0264537f798b4c875f1534a3093e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 14:25:09 +0000 Subject: [PATCH 63/90] chore(deps): update dependency @types/node to v16.18.12 Signed-off-by: Renovate Bot --- yarn.lock | 164 +++++++++++++++++++++++++++--------------------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/yarn.lock b/yarn.lock index 15cc1e24a9..259b321cd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3362,7 +3362,7 @@ __metadata: "@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/node": ^16.0.0 "@types/react": ^16.13.1 || ^17.0.0 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -3702,7 +3702,7 @@ __metadata: "@types/jest": ^29.0.0 "@types/minimatch": ^5.0.0 "@types/mock-fs": ^4.13.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/npm-packlist": ^3.0.0 "@types/recursive-readdir": ^2.2.0 "@types/rollup-plugin-peer-deps-external": ^2.2.0 @@ -3804,7 +3804,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@types/jscodeshift": ^0.11.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 chalk: ^4.0.0 commander: ^9.1.0 jscodeshift: ^0.14.0 @@ -3827,7 +3827,7 @@ __metadata: "@types/json-schema": ^7.0.6 "@types/json-schema-merge-allof": ^0.6.0 "@types/mock-fs": ^4.10.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/yup": ^0.29.13 ajv: ^8.10.0 chokidar: ^3.5.2 @@ -3870,7 +3870,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/prop-types": ^15.7.3 "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 @@ -3968,7 +3968,7 @@ __metadata: "@types/d3-zoom": ^3.0.1 "@types/dagre": ^0.7.44 "@types/google-protobuf": ^3.7.2 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react-helmet": ^6.1.0 "@types/react-sparklines": ^1.7.0 "@types/react-syntax-highlighter": ^15.0.0 @@ -4025,7 +4025,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/prop-types": ^15.7.3 "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 @@ -4049,7 +4049,7 @@ __metadata: "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 "@types/inquirer": ^8.1.3 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/recursive-readdir": ^2.2.0 chalk: ^4.0.0 commander: ^9.1.0 @@ -4174,7 +4174,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4266,7 +4266,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/git-url-parse": ^9.0.0 - "@types/node": "*" + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-markdown: ^8.0.0 @@ -4318,7 +4318,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4347,7 +4347,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4375,7 +4375,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-ga: ^3.3.0 @@ -4401,7 +4401,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.10.1 @@ -4445,7 +4445,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/swagger-ui-react": ^4.1.1 cross-fetch: ^3.1.5 graphiql: ^1.8.8 @@ -4480,7 +4480,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4656,7 +4656,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 humanize-duration: ^3.27.0 luxon: ^3.0.0 @@ -4718,7 +4718,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -4770,7 +4770,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4862,7 +4862,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 lodash: ^4.17.21 @@ -5560,7 +5560,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.25.1 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 circleci-api: ^4.0.0 cross-fetch: ^3.1.5 humanize-duration: ^3.27.0 @@ -5593,7 +5593,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -5625,7 +5625,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.27.1 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 humanize-duration: ^3.27.1 luxon: ^3.0.0 @@ -5687,7 +5687,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/highlightjs": ^10.1.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 highlight.js: ^10.6.0 @@ -5720,7 +5720,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 rc-progress: 3.4.1 @@ -5751,7 +5751,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 jsonschema: ^1.2.6 msw: ^0.49.0 @@ -5792,7 +5792,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/pluralize": ^0.0.29 "@types/recharts": ^1.8.14 "@types/regression": ^2.0.0 @@ -5834,7 +5834,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.0.0 express: ^4.18.1 msw: ^0.49.0 react-use: ^17.2.4 @@ -6017,7 +6017,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 languageName: unknown @@ -6048,7 +6048,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 classnames: ^2.2.6 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -6078,7 +6078,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6108,7 +6108,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6140,7 +6140,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/dompurify": ^2.3.3 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/sanitize-html": ^2.6.2 classnames: ^2.3.1 cross-fetch: ^3.1.5 @@ -6173,7 +6173,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 peerDependencies: @@ -6202,7 +6202,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6238,7 +6238,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6272,7 +6272,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6304,7 +6304,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.0.0 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6336,7 +6336,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6366,7 +6366,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -6398,7 +6398,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/lodash": ^4.14.173 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 @@ -6428,7 +6428,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/codemirror": ^5.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 graphiql: ^1.5.12 graphql: ^16.0.0 @@ -6484,7 +6484,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 graphql-voyager: ^1.0.0-rc.31 msw: ^0.49.0 @@ -6538,7 +6538,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^0.49.0 @@ -6572,7 +6572,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 humanize-duration: ^3.26.0 luxon: ^3.0.0 @@ -6640,7 +6640,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/testing-library__jest-dom": ^5.9.1 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6696,7 +6696,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 jest-when: ^3.1.0 msw: ^0.49.0 @@ -6784,7 +6784,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react": ^16.13.1 || ^17.0.0 cronstrue: ^2.2.0 cross-fetch: ^3.1.5 @@ -6846,7 +6846,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -6897,7 +6897,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -6926,7 +6926,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -6955,7 +6955,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 p-limit: ^3.1.0 @@ -6988,7 +6988,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7042,7 +7042,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -7203,7 +7203,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^0.49.0 @@ -7290,7 +7290,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 @@ -7342,7 +7342,7 @@ __metadata: "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 "@types/mock-fs": ^4.13.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 command-exists: ^1.2.9 fs-extra: ^10.0.1 jest-when: ^3.1.0 @@ -7556,7 +7556,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.18.1 "@types/json-schema": ^7.0.9 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@uiw/react-codemirror": ^4.9.3 classnames: ^2.2.6 cross-fetch: ^3.1.5 @@ -7761,7 +7761,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.9.4 @@ -7794,7 +7794,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7825,7 +7825,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/zen-observable": ^0.8.2 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -7893,7 +7893,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 rc-progress: 3.4.1 @@ -7923,7 +7923,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7997,7 +7997,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^0.49.0 @@ -8109,7 +8109,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.9.4 @@ -8140,7 +8140,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/color": ^3.0.1 "@types/d3-force": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react": ^16.13.1 || ^17.0.0 color: ^4.0.1 cross-fetch: ^3.1.5 @@ -8175,7 +8175,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -8243,7 +8243,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 git-url-parse: ^13.0.0 @@ -8356,7 +8356,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/dompurify": ^2.2.2 "@types/event-source-polyfill": ^1.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 canvas: ^2.10.2 cross-fetch: ^3.1.5 dompurify: ^2.2.9 @@ -8418,7 +8418,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -8471,7 +8471,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -8530,7 +8530,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -8558,7 +8558,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 @@ -8633,7 +8633,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 zen-observable: ^0.10.0 @@ -10546,7 +10546,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/test-utils": "workspace:^" - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 languageName: unknown @@ -10569,7 +10569,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: @@ -13491,7 +13491,7 @@ __metadata: "@types/dockerode": ^3.3.0 "@types/fs-extra": ^9.0.6 "@types/http-proxy": ^1.17.4 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/serve-handler": ^6.1.0 "@types/webpack-env": ^1.15.3 commander: ^9.1.0 @@ -14841,7 +14841,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^16.0.0, @types/node@npm:^16.11.26, @types/node@npm:^16.9.2": +"@types/node@npm:^16.0.0, @types/node@npm:^16.9.2": version: 16.18.11 resolution: "@types/node@npm:16.18.11" checksum: 2a3b1da13063debe6e26f732defb5f03ef4ef732c3e08daba838d8850433bd00e537ce1a97ce9bcfc4b15db5218d701d1265fae94e0d6926906bec157e6b46e0 @@ -20874,7 +20874,7 @@ __metadata: "@backstage/cli-common": "workspace:^" "@backstage/errors": "workspace:^" "@types/fs-extra": ^9.0.1 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/puppeteer": ^5.4.4 chalk: ^4.0.0 commander: ^9.1.0 @@ -22194,7 +22194,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/jquery": ^3.3.34 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react": "*" "@types/react-dom": "*" "@types/zen-observable": ^0.8.0 @@ -34251,7 +34251,7 @@ __metadata: "@spotify/eslint-plugin": ^14.1.3 "@spotify/prettier-config": ^14.0.0 "@techdocs/cli": "workspace:*" - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/webpack": ^5.28.0 command-exists: ^1.2.9 concurrently: ^7.0.0 @@ -36275,7 +36275,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": ^16.0.0 "@types/react-dom": "*" cross-env: ^7.0.0 cypress: ^10.0.0 From 5b5d7d416ffca72ce84d0bf844a3938a9aacf777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C5=A0neberger?= Date: Mon, 6 Feb 2023 15:27:22 +0100 Subject: [PATCH 64/90] Update changeset with migration diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marek Šneberger --- .changeset/hip-chairs-double.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.changeset/hip-chairs-double.md b/.changeset/hip-chairs-double.md index 73e939ba21..722795d80d 100644 --- a/.changeset/hip-chairs-double.md +++ b/.changeset/hip-chairs-double.md @@ -3,3 +3,18 @@ --- Deprecate `RadarEntry.url` - use `RadarEntry.links` instead + +```diff +- url: 'https://www.javascript.com/', ++ url: '#', + key: 'javascript', + id: 'javascript', + title: 'JavaScript', + quadrant: 'languages', + links: [ ++ { ++ url: 'https://www.javascript.com/', ++ title: 'Learn more', ++ }, + ], +``` From a6aff183ba79ebb4531d30a2a6d89b9ae3013ce1 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 15:28:43 +0100 Subject: [PATCH 65/90] chore: updating the breaking changes Signed-off-by: blam --- .github/workflows/automate_changeset_feedback.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index 11fef75b9b..aaf0bb1294 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -31,10 +31,10 @@ jobs: - uses: backstage/actions/changeset-feedback@v0.6.0 name: Generate feedback with: - diffRef: 'origin/master' + diff-ref: 'origin/master' marker: issue-number: ${{ github.event.pull_request.number }} - botUsername: backstage-goalie[bot] + bot-username: backstage-goalie[bot] app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }} From cd065df85f7814e3d586e868e01cf37f39f5cc77 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 14:35:46 +0000 Subject: [PATCH 66/90] chore(deps): update dependency @types/node to v18.11.19 Signed-off-by: Renovate Bot --- yarn.lock | 160 +++++++++++++++++++++++++++--------------------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/yarn.lock b/yarn.lock index 15cc1e24a9..a3e4f754b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3362,7 +3362,7 @@ __metadata: "@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/node": "*" "@types/react": ^16.13.1 || ^17.0.0 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -3652,7 +3652,7 @@ __metadata: resolution: "@backstage/cli-common@workspace:packages/cli-common" dependencies: "@backstage/cli": "workspace:^" - "@types/node": ^16.0.0 + "@types/node": "*" languageName: unknown linkType: soft @@ -3702,7 +3702,7 @@ __metadata: "@types/jest": ^29.0.0 "@types/minimatch": ^5.0.0 "@types/mock-fs": ^4.13.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/npm-packlist": ^3.0.0 "@types/recursive-readdir": ^2.2.0 "@types/rollup-plugin-peer-deps-external": ^2.2.0 @@ -3804,7 +3804,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@types/jscodeshift": ^0.11.0 - "@types/node": ^16.11.26 + "@types/node": "*" chalk: ^4.0.0 commander: ^9.1.0 jscodeshift: ^0.14.0 @@ -3827,7 +3827,7 @@ __metadata: "@types/json-schema": ^7.0.6 "@types/json-schema-merge-allof": ^0.6.0 "@types/mock-fs": ^4.10.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/yup": ^0.29.13 ajv: ^8.10.0 chokidar: ^3.5.2 @@ -3851,7 +3851,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" - "@types/node": ^16.0.0 + "@types/node": "*" lodash: ^4.17.21 languageName: unknown linkType: soft @@ -3870,7 +3870,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/prop-types": ^15.7.3 "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 @@ -3968,7 +3968,7 @@ __metadata: "@types/d3-zoom": ^3.0.1 "@types/dagre": ^0.7.44 "@types/google-protobuf": ^3.7.2 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react-helmet": ^6.1.0 "@types/react-sparklines": ^1.7.0 "@types/react-syntax-highlighter": ^15.0.0 @@ -4025,7 +4025,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/prop-types": ^15.7.3 "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 @@ -4049,7 +4049,7 @@ __metadata: "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 "@types/inquirer": ^8.1.3 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/recursive-readdir": ^2.2.0 chalk: ^4.0.0 commander: ^9.1.0 @@ -4085,7 +4085,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": "*" react-use: ^17.2.4 zen-observable: ^0.10.0 peerDependencies: @@ -4174,7 +4174,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4318,7 +4318,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4347,7 +4347,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4375,7 +4375,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-ga: ^3.3.0 @@ -4401,7 +4401,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.10.1 @@ -4445,7 +4445,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/swagger-ui-react": ^4.1.1 cross-fetch: ^3.1.5 graphiql: ^1.8.8 @@ -4656,7 +4656,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 humanize-duration: ^3.27.0 luxon: ^3.0.0 @@ -4718,7 +4718,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -4770,7 +4770,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4862,7 +4862,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 lodash: ^4.17.21 @@ -5560,7 +5560,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.25.1 - "@types/node": ^16.11.26 + "@types/node": "*" circleci-api: ^4.0.0 cross-fetch: ^3.1.5 humanize-duration: ^3.27.0 @@ -5593,7 +5593,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -5625,7 +5625,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.27.1 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 humanize-duration: ^3.27.1 luxon: ^3.0.0 @@ -5687,7 +5687,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/highlightjs": ^10.1.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 highlight.js: ^10.6.0 @@ -5751,7 +5751,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 jsonschema: ^1.2.6 msw: ^0.49.0 @@ -5792,7 +5792,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/pluralize": ^0.0.29 "@types/recharts": ^1.8.14 "@types/regression": ^2.0.0 @@ -6017,7 +6017,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 languageName: unknown @@ -6048,7 +6048,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" classnames: ^2.2.6 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -6078,7 +6078,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6108,7 +6108,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6140,7 +6140,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/dompurify": ^2.3.3 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/sanitize-html": ^2.6.2 classnames: ^2.3.1 cross-fetch: ^3.1.5 @@ -6173,7 +6173,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 peerDependencies: @@ -6202,7 +6202,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6238,7 +6238,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6272,7 +6272,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6336,7 +6336,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6366,7 +6366,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -6398,7 +6398,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/lodash": ^4.14.173 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 @@ -6428,7 +6428,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/codemirror": ^5.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 graphiql: ^1.5.12 graphql: ^16.0.0 @@ -6538,7 +6538,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^0.49.0 @@ -6572,7 +6572,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 humanize-duration: ^3.26.0 luxon: ^3.0.0 @@ -6640,7 +6640,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/testing-library__jest-dom": ^5.9.1 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6696,7 +6696,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 jest-when: ^3.1.0 msw: ^0.49.0 @@ -6784,7 +6784,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react": ^16.13.1 || ^17.0.0 cronstrue: ^2.2.0 cross-fetch: ^3.1.5 @@ -6846,7 +6846,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -6897,7 +6897,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -6955,7 +6955,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 p-limit: ^3.1.0 @@ -6988,7 +6988,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7042,7 +7042,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -7290,7 +7290,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 @@ -7342,7 +7342,7 @@ __metadata: "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 "@types/mock-fs": ^4.13.0 - "@types/node": ^16.11.26 + "@types/node": "*" command-exists: ^1.2.9 fs-extra: ^10.0.1 jest-when: ^3.1.0 @@ -7556,7 +7556,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.18.1 "@types/json-schema": ^7.0.9 - "@types/node": ^16.11.26 + "@types/node": "*" "@uiw/react-codemirror": ^4.9.3 classnames: ^2.2.6 cross-fetch: ^3.1.5 @@ -7761,7 +7761,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.9.4 @@ -7794,7 +7794,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7825,7 +7825,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/zen-observable": ^0.8.2 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -7893,7 +7893,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 rc-progress: 3.4.1 @@ -7923,7 +7923,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7997,7 +7997,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^0.49.0 @@ -8109,7 +8109,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.9.4 @@ -8140,7 +8140,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/color": ^3.0.1 "@types/d3-force": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react": ^16.13.1 || ^17.0.0 color: ^4.0.1 cross-fetch: ^3.1.5 @@ -8175,7 +8175,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -8243,7 +8243,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 git-url-parse: ^13.0.0 @@ -8356,7 +8356,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/dompurify": ^2.2.2 "@types/event-source-polyfill": ^1.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" canvas: ^2.10.2 cross-fetch: ^3.1.5 dompurify: ^2.2.9 @@ -8418,7 +8418,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -8471,7 +8471,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -8558,7 +8558,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 @@ -8576,7 +8576,7 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/test-utils": "workspace:^" - "@types/node": ^16.0.0 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 languageName: unknown @@ -8633,7 +8633,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 zen-observable: ^0.10.0 @@ -10546,7 +10546,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/test-utils": "workspace:^" - "@types/node": ^16.11.26 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 languageName: unknown @@ -10569,7 +10569,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: @@ -13491,7 +13491,7 @@ __metadata: "@types/dockerode": ^3.3.0 "@types/fs-extra": ^9.0.6 "@types/http-proxy": ^1.17.4 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/serve-handler": ^6.1.0 "@types/webpack-env": ^1.15.3 commander: ^9.1.0 @@ -14800,9 +14800,9 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>= 8, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^18.11.17": - version: 18.11.18 - resolution: "@types/node@npm:18.11.18" - checksum: 03f17f9480f8d775c8a72da5ea7e9383db5f6d85aa5fefde90dd953a1449bd5e4ffde376f139da4f3744b4c83942166d2a7603969a6f8ea826edfb16e6e3b49d + version: 18.11.19 + resolution: "@types/node@npm:18.11.19" + checksum: d7cd19fcfc59cbdd3f9ba0b4072cb7adc21bd575bd8eb7d7e698975e63564aaa83f03434f32b12331f84f73d0b369d9cbe2371e359d9d7f5c3361f4987f4f7da languageName: node linkType: hard @@ -14841,7 +14841,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^16.0.0, @types/node@npm:^16.11.26, @types/node@npm:^16.9.2": +"@types/node@npm:^16.9.2": version: 16.18.11 resolution: "@types/node@npm:16.18.11" checksum: 2a3b1da13063debe6e26f732defb5f03ef4ef732c3e08daba838d8850433bd00e537ce1a97ce9bcfc4b15db5218d701d1265fae94e0d6926906bec157e6b46e0 @@ -20874,7 +20874,7 @@ __metadata: "@backstage/cli-common": "workspace:^" "@backstage/errors": "workspace:^" "@types/fs-extra": ^9.0.1 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/puppeteer": ^5.4.4 chalk: ^4.0.0 commander: ^9.1.0 @@ -22194,7 +22194,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/jquery": ^3.3.34 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react": "*" "@types/react-dom": "*" "@types/zen-observable": ^0.8.0 @@ -34251,7 +34251,7 @@ __metadata: "@spotify/eslint-plugin": ^14.1.3 "@spotify/prettier-config": ^14.0.0 "@techdocs/cli": "workspace:*" - "@types/node": ^16.11.26 + "@types/node": "*" "@types/webpack": ^5.28.0 command-exists: ^1.2.9 concurrently: ^7.0.0 @@ -36275,7 +36275,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.11.26 + "@types/node": "*" "@types/react-dom": "*" cross-env: ^7.0.0 cypress: ^10.0.0 From 6c54bf08a5a040dce86758e51a193615a5d68b34 Mon Sep 17 00:00:00 2001 From: Lee Mills Date: Mon, 6 Feb 2023 10:49:28 +0000 Subject: [PATCH 67/90] Adding whats ahead blog post and image asset Signed-off-by: Lee Mills --- .../blog/2023-02-06-whats-ahead-in-2023.md | 58 ++++++++++++++++++ .../blog/assets/2023-02-06/whats-ahead.png | Bin 0 -> 150199 bytes 2 files changed, 58 insertions(+) create mode 100644 microsite/blog/2023-02-06-whats-ahead-in-2023.md create mode 100644 microsite/blog/assets/2023-02-06/whats-ahead.png diff --git a/microsite/blog/2023-02-06-whats-ahead-in-2023.md b/microsite/blog/2023-02-06-whats-ahead-in-2023.md new file mode 100644 index 0000000000..a9f06d9ea0 --- /dev/null +++ b/microsite/blog/2023-02-06-whats-ahead-in-2023.md @@ -0,0 +1,58 @@ +--- +title: What’s Ahead in 2023? +author: Lee Mills +authorURL: https://github.com/leemills83 +--- + +# What’s Ahead in 2023? + +![Fewer “vibes” and more climbs (up the contribution ladder)](assets/2023-02-06/whats-ahead.png) + +Okay, everyone. Time to move on from all the 2022 reflections, Top 10 lists, and Wrapped(s), and turn our gaze in the other direction — towards 2023. As the Backstage team moves into the new year, we find ourselves asking those perennial questions: How can we continue to grow and enhance the Backstage community? How can we improve the experience for those adopting Backstage? How can we give the best support to Backstage contributors? + +Well, we don’t have all the answers, but we do have some. So we wanted to share more on three ongoing initiatives aimed at moving the needle forward for these three distinct, yet interrelated, questions. + +- Grow your influence by climbing the contribution ladder +- Come join our SIGs (and push for new ones) +- Work with us to drive a better experience for Backstage adopters + +# Climb the contribution ladder + +Truth be told, we’ve been wanting to grow the maintainer team for some time, and have had multiple requests from contributors and contributing organizations wanting to become maintainers. But, so far, we just haven’t managed to get anything off the ground. What we see is that becoming a maintainer is too hard and the current ownership model doesn’t work. In short, we need to update Backstage governance. + +To address this, we are proposing (RFC links below) the following three initiatives: + +- Introduce more steps in between first-time contributor and maintainer in the form of a contributor ladder (loosely based on the [CNCF Project Template](https://github.com/cncf/project-template/blob/main/CONTRIBUTOR_LADDER.md)). The ladder might look as follows: Contributor, Org Member, Reviewer, Maintainer, Project Maintainer. +- Change the ownership model, for example, divide the project up into several project areas, each covering particular parts of the project. +- Introduce a new process for large contributions in the form of Backstage Enhancement Proposals (BEPs). + +Read more and join the already rich discussion in [RFC: Updated Backstage Governance](https://github.com/backstage/backstage/issues/15317) and [RFC: Backstage Enhancement Proposals](https://github.com/backstage/backstage/issues/15318). The RFC will be open until 13/02/2023. + +# Come join our SIGs (and push for new ones) + +As many of you know, SIG stands for Special Interest Group. The purpose of a SIG is to bring together maintainers and interested contributors within a certain specialist area to discuss, debate, spread knowledge, brainstorm - and drive the overall project forward. They’re also a lot of fun! + +We currently have two SIGs up and running: + +- [Catalog](https://github.com/backstage/community/tree/main/sigs/sig-catalog?utm_campaign=Newsletters%20Sent&utm_medium=email&_hsmi=241130993&_hsenc=p2ANqtz-9fx-nTmQga2j12NqzZaL-lYPq-xQjDoXcWKe_JUoa2RGlK65R9j0CIygfYILr0tGEQSvRVN9xzhiJ6LmVxBthKnStfSA&utm_content=241130993&utm_source=hs_email) started up last June and covers all aspects of the systems and components related to the Software Catalog. The group runs every second Tuesday between 16:00 and 17:00 CET. Just show up or hop into [#catalog](https://discord.com/channels/687207715902193673/923144214580191282) (Discord), if you want to talk to us first. Here are the [notes from our sessions so far](https://docs.google.com/document/d/1xg57CDfX2R5jTGbbWANzifm2szEb664kuTwfEEvKgZU/edit#heading=h.iw0hjv6xfb9e). +- [Adoption](https://github.com/backstage/community/tree/main/sigs/sig-adoption?utm_campaign=Newsletters%20Sent&utm_medium=email&_hsmi=241130993&_hsenc=p2ANqtz-9CAxtT8JSeJZPh41o3ZnDBi0VBFpbL6fEqXInr9HHOFhU3JAo8HekKg1JzE5rhGP5zOaHcsYDd1YxJWNHw-eSk5mLWkA&utm_content=241130993&utm_source=hs_email) kicked off in December and covers all aspects of the Backstage adoption journey including documentation and tutorials. In the first two meetups, we discussed adoption challenges based on user persona, collecting data about those challenges, and bootstrapping the Catalog to improve metadata quality for adopters. This one runs every second Thursday between 16:00 and 17:00 CET. Again just show up or, if you want to talk to us first, you can do so in [#adoption](https://discord.com/channels/687207715902193673/1045031039061479536) (Discord). Here are the [notes from our sessions](https://docs.google.com/document/d/1zeYSzXYgh897bxPQmwy2btOwCFlEjlUDLf00riySLRc/edit?_hsmi=241130993&_hsenc=p2ANqtz-_nPuE5hW4fnbhJ5-uK8r5HisSTU7rkslKirvqu0g-vdynbitPVYF2AVLAatos2dLoxb2Bgx_g4LBgH02kXoq7_Acd43Q#heading=h.iw0hjv6xfb9e) so far. + +So that’s SIGs. Get involved and join the ones we have up and running. And/or feel free to propose new ones. + +# Help drive a better experience for all adopters + +Throughout all stages of adopting Backstage in an organization — initial discovery, evaluation and POC, and scaling to the wider org — we have identified opportunities where we want to make the journey for Backstage adopters faster and smoother. + +We’ll go step-by-step, but initial plans are to: + +- Make it easier to install, customize, and maintain Backstage +- Provide Golden Paths for adoption +- Improve the experience and the content on [backstage.io](https://backstage.io/docs/overview/what-is-backstage) + +As we move forward, we’d love you to be part of it. Here are some ways that you can contribute to this initiative: + +- Simply give us feedback. [Tell us about your adoption journey](https://spotify.co1.qualtrics.com/jfe/form/SV_cuxhYPB6oQDu7gW). +- Contribute to the pages and documentation on [backstage.io](https://backstage.io/docs/overview/what-is-backstage) +- Pay it forward by making code contributions even during your adoption journey (if you are on one) +- Join the [Adoption](https://github.com/backstage/community/tree/main/sigs/sig-adoption?utm_campaign=Newsletters%20Sent&utm_medium=email&_hsmi=241130993&_hsenc=p2ANqtz-9CAxtT8JSeJZPh41o3ZnDBi0VBFpbL6fEqXInr9HHOFhU3JAo8HekKg1JzE5rhGP5zOaHcsYDd1YxJWNHw-eSk5mLWkA&utm_content=241130993&utm_source=hs_email) SIG +- Chat with us on [#adoption](https://discord.com/channels/687207715902193673/1045031039061479536) (Discord) diff --git a/microsite/blog/assets/2023-02-06/whats-ahead.png b/microsite/blog/assets/2023-02-06/whats-ahead.png new file mode 100644 index 0000000000000000000000000000000000000000..e49f68149d7cf72acf5d06b008b4cd3b74660d18 GIT binary patch literal 150199 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

Y=)q0dY> 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ÇHS?RntQ1I!I9``-y(E;j&0z9)hLcgCb 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 S6#*Toaz$QMF6*-4!~X+pLLrC% literal 0 HcmV?d00001 From 922c7f85347e5d467d260f5fe7e3f29de3f54ced Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 15:52:11 +0100 Subject: [PATCH 68/90] chore: add BEP's and sort Signed-off-by: blam --- .github/vale/Vocab/Backstage/accept.txt | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index 9e7cf7aa98..d933d2889a 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -9,6 +9,8 @@ Airbrake Airbrakes Alaria Alef +allowlisted +allowlisted Anddddd Apdex api @@ -23,6 +25,7 @@ Avro backend's backported backporting +BEPs Bigtable Billett bitbucket @@ -95,6 +98,8 @@ dockerfiles Dockerize dockerode Docusaurus +Dominik +Dominik DOMPurify don'ts dynatrace @@ -119,6 +124,8 @@ Firekube Firestore Fiverr Francesco +Frontside +Frontside gerrit Gerrit gitbeaker @@ -138,6 +145,8 @@ Hackathons haproxy hardcoded Helidon +Henneke +Henneke Heroku hoc horizontalpodautoscalers @@ -167,12 +176,18 @@ jsonnet jsx JWTs Kaewkasi +Kaswell +Kaswell Keyv Knex KPIs +Kuang +Kuang kubectl kubernetes kubernetes +Lainfiesta +Lainfiesta ldap learnings Leasot @@ -413,10 +428,3 @@ zod Zolotusky zoomable zsh -Lainfiesta -allowlisted -Dominik -Henneke -Kuang -Frontside -Kaswell From 3ee310ce1ff48e4c8642718ea8f09c20a67af249 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 15:54:51 +0100 Subject: [PATCH 69/90] chore: fix duplication and bad merge Signed-off-by: blam --- .github/vale/Vocab/Backstage/accept.txt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index d933d2889a..e6380a17f0 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -10,7 +10,6 @@ Airbrakes Alaria Alef allowlisted -allowlisted Anddddd Apdex api @@ -99,7 +98,6 @@ Dockerize dockerode Docusaurus Dominik -Dominik DOMPurify don'ts dynatrace @@ -125,7 +123,6 @@ Firestore Fiverr Francesco Frontside -Frontside gerrit Gerrit gitbeaker @@ -146,7 +143,6 @@ haproxy hardcoded Helidon Henneke -Henneke Heroku hoc horizontalpodautoscalers @@ -177,16 +173,12 @@ jsx JWTs Kaewkasi Kaswell -Kaswell Keyv Knex KPIs Kuang -Kuang kubectl kubernetes -kubernetes -Lainfiesta Lainfiesta ldap learnings From bfd441b9d5f4dbba84ca30749e0822d38df110bf Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 16:05:28 +0100 Subject: [PATCH 70/90] chore: fixing bad changeset ] Signed-off-by: blam --- .changeset/slow-moles-sin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/slow-moles-sin.md b/.changeset/slow-moles-sin.md index 140d9e7435..02e1952312 100644 --- a/.changeset/slow-moles-sin.md +++ b/.changeset/slow-moles-sin.md @@ -2,4 +2,4 @@ '@backstage/plugin-circleci': patch --- -Making workflow a link +Making workflow a link From e6b80909385fe5b7ab03c32e95e43d0f8d9ea4d0 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 16:50:42 +0100 Subject: [PATCH 71/90] chore: fix yarn install Signed-off-by: blam --- yarn.lock | 170 +++++++++++++++++++++++++++--------------------------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/yarn.lock b/yarn.lock index 259b321cd5..04738e5b2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3362,7 +3362,7 @@ __metadata: "@material-ui/icons": ^4.9.1 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -3702,7 +3702,7 @@ __metadata: "@types/jest": ^29.0.0 "@types/minimatch": ^5.0.0 "@types/mock-fs": ^4.13.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/npm-packlist": ^3.0.0 "@types/recursive-readdir": ^2.2.0 "@types/rollup-plugin-peer-deps-external": ^2.2.0 @@ -3804,7 +3804,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@types/jscodeshift": ^0.11.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 chalk: ^4.0.0 commander: ^9.1.0 jscodeshift: ^0.14.0 @@ -3827,7 +3827,7 @@ __metadata: "@types/json-schema": ^7.0.6 "@types/json-schema-merge-allof": ^0.6.0 "@types/mock-fs": ^4.10.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/yup": ^0.29.13 ajv: ^8.10.0 chokidar: ^3.5.2 @@ -3870,7 +3870,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/prop-types": ^15.7.3 "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 @@ -3968,7 +3968,7 @@ __metadata: "@types/d3-zoom": ^3.0.1 "@types/dagre": ^0.7.44 "@types/google-protobuf": ^3.7.2 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react-helmet": ^6.1.0 "@types/react-sparklines": ^1.7.0 "@types/react-syntax-highlighter": ^15.0.0 @@ -4025,7 +4025,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/prop-types": ^15.7.3 "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 @@ -4049,7 +4049,7 @@ __metadata: "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 "@types/inquirer": ^8.1.3 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/recursive-readdir": ^2.2.0 chalk: ^4.0.0 commander: ^9.1.0 @@ -4174,7 +4174,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4266,7 +4266,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/git-url-parse": ^9.0.0 - "@types/node": ^16.0.0 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-markdown: ^8.0.0 @@ -4318,7 +4318,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4347,7 +4347,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4375,7 +4375,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-ga: ^3.3.0 @@ -4401,7 +4401,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.10.1 @@ -4445,7 +4445,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/swagger-ui-react": ^4.1.1 cross-fetch: ^3.1.5 graphiql: ^1.8.8 @@ -4480,7 +4480,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4656,7 +4656,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 humanize-duration: ^3.27.0 luxon: ^3.0.0 @@ -4718,7 +4718,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -4770,7 +4770,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4862,7 +4862,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 lodash: ^4.17.21 @@ -5560,7 +5560,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.25.1 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 circleci-api: ^4.0.0 cross-fetch: ^3.1.5 humanize-duration: ^3.27.0 @@ -5593,7 +5593,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -5625,7 +5625,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.27.1 "@types/luxon": ^3.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 humanize-duration: ^3.27.1 luxon: ^3.0.0 @@ -5687,7 +5687,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/highlightjs": ^10.1.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 highlight.js: ^10.6.0 @@ -5720,7 +5720,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 rc-progress: 3.4.1 @@ -5751,7 +5751,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 jsonschema: ^1.2.6 msw: ^0.49.0 @@ -5792,7 +5792,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/pluralize": ^0.0.29 "@types/recharts": ^1.8.14 "@types/regression": ^2.0.0 @@ -5834,7 +5834,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": "*" express: ^4.18.1 msw: ^0.49.0 react-use: ^17.2.4 @@ -6017,7 +6017,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 languageName: unknown @@ -6048,7 +6048,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 classnames: ^2.2.6 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -6078,7 +6078,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6108,7 +6108,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6140,7 +6140,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/dompurify": ^2.3.3 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/sanitize-html": ^2.6.2 classnames: ^2.3.1 cross-fetch: ^3.1.5 @@ -6173,7 +6173,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 peerDependencies: @@ -6202,7 +6202,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6238,7 +6238,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6272,7 +6272,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6304,7 +6304,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": "*" "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6336,7 +6336,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6366,7 +6366,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -6398,7 +6398,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/lodash": ^4.14.173 "@types/luxon": ^3.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 @@ -6428,7 +6428,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/codemirror": ^5.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 graphiql: ^1.5.12 graphql: ^16.0.0 @@ -6484,7 +6484,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": "*" cross-fetch: ^3.1.5 graphql-voyager: ^1.0.0-rc.31 msw: ^0.49.0 @@ -6538,7 +6538,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^0.49.0 @@ -6572,7 +6572,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 humanize-duration: ^3.26.0 luxon: ^3.0.0 @@ -6640,7 +6640,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/testing-library__jest-dom": ^5.9.1 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6696,7 +6696,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 jest-when: ^3.1.0 msw: ^0.49.0 @@ -6784,7 +6784,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cronstrue: ^2.2.0 cross-fetch: ^3.1.5 @@ -6846,7 +6846,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -6897,7 +6897,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -6926,7 +6926,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -6955,7 +6955,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 p-limit: ^3.1.0 @@ -6988,7 +6988,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7042,7 +7042,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -7203,7 +7203,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": "*" cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^0.49.0 @@ -7290,7 +7290,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 @@ -7342,7 +7342,7 @@ __metadata: "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 "@types/mock-fs": ^4.13.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 command-exists: ^1.2.9 fs-extra: ^10.0.1 jest-when: ^3.1.0 @@ -7556,7 +7556,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.18.1 "@types/json-schema": ^7.0.9 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@uiw/react-codemirror": ^4.9.3 classnames: ^2.2.6 cross-fetch: ^3.1.5 @@ -7761,7 +7761,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.9.4 @@ -7794,7 +7794,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7825,7 +7825,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/zen-observable": ^0.8.2 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -7893,7 +7893,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 rc-progress: 3.4.1 @@ -7923,7 +7923,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7997,7 +7997,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^0.49.0 @@ -8109,7 +8109,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.9.4 @@ -8140,7 +8140,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/color": ^3.0.1 "@types/d3-force": ^3.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 color: ^4.0.1 cross-fetch: ^3.1.5 @@ -8175,7 +8175,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -8243,7 +8243,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 git-url-parse: ^13.0.0 @@ -8356,7 +8356,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/dompurify": ^2.2.2 "@types/event-source-polyfill": ^1.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 canvas: ^2.10.2 cross-fetch: ^3.1.5 dompurify: ^2.2.9 @@ -8418,7 +8418,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -8471,7 +8471,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -8530,7 +8530,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": "*" cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -8558,7 +8558,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 @@ -8633,7 +8633,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 zen-observable: ^0.10.0 @@ -10546,7 +10546,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/test-utils": "workspace:^" - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 languageName: unknown @@ -10569,7 +10569,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: @@ -13491,7 +13491,7 @@ __metadata: "@types/dockerode": ^3.3.0 "@types/fs-extra": ^9.0.6 "@types/http-proxy": ^1.17.4 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/serve-handler": ^6.1.0 "@types/webpack-env": ^1.15.3 commander: ^9.1.0 @@ -14841,10 +14841,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^16.0.0, @types/node@npm:^16.9.2": - version: 16.18.11 - resolution: "@types/node@npm:16.18.11" - checksum: 2a3b1da13063debe6e26f732defb5f03ef4ef732c3e08daba838d8850433bd00e537ce1a97ce9bcfc4b15db5218d701d1265fae94e0d6926906bec157e6b46e0 +"@types/node@npm:^16.0.0, @types/node@npm:^16.11.26, @types/node@npm:^16.9.2": + version: 16.18.12 + resolution: "@types/node@npm:16.18.12" + checksum: fc3271182414f8593018ef8f00b4718116a92f463f619081bd399d9460e7861e1dd7eebc7cf94c23567e418ff397babed077011711aae8d47171b5a81c5bd71d languageName: node linkType: hard @@ -20874,7 +20874,7 @@ __metadata: "@backstage/cli-common": "workspace:^" "@backstage/errors": "workspace:^" "@types/fs-extra": ^9.0.1 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/puppeteer": ^5.4.4 chalk: ^4.0.0 commander: ^9.1.0 @@ -22194,7 +22194,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/jquery": ^3.3.34 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react": "*" "@types/react-dom": "*" "@types/zen-observable": ^0.8.0 @@ -34251,7 +34251,7 @@ __metadata: "@spotify/eslint-plugin": ^14.1.3 "@spotify/prettier-config": ^14.0.0 "@techdocs/cli": "workspace:*" - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/webpack": ^5.28.0 command-exists: ^1.2.9 concurrently: ^7.0.0 @@ -36275,7 +36275,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": ^16.0.0 + "@types/node": ^16.11.26 "@types/react-dom": "*" cross-env: ^7.0.0 cypress: ^10.0.0 From 3264529162871c77a6b4c40b8499702e18834518 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 16:55:59 +0100 Subject: [PATCH 72/90] chore: fix yarn install Signed-off-by: blam --- yarn.lock | 160 +++++++++++++++++++++++++++--------------------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/yarn.lock b/yarn.lock index a3e4f754b2..83503fb359 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3362,7 +3362,7 @@ __metadata: "@material-ui/icons": ^4.9.1 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -3652,7 +3652,7 @@ __metadata: resolution: "@backstage/cli-common@workspace:packages/cli-common" dependencies: "@backstage/cli": "workspace:^" - "@types/node": "*" + "@types/node": ^16.0.0 languageName: unknown linkType: soft @@ -3702,7 +3702,7 @@ __metadata: "@types/jest": ^29.0.0 "@types/minimatch": ^5.0.0 "@types/mock-fs": ^4.13.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/npm-packlist": ^3.0.0 "@types/recursive-readdir": ^2.2.0 "@types/rollup-plugin-peer-deps-external": ^2.2.0 @@ -3804,7 +3804,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@types/jscodeshift": ^0.11.0 - "@types/node": "*" + "@types/node": ^16.11.26 chalk: ^4.0.0 commander: ^9.1.0 jscodeshift: ^0.14.0 @@ -3827,7 +3827,7 @@ __metadata: "@types/json-schema": ^7.0.6 "@types/json-schema-merge-allof": ^0.6.0 "@types/mock-fs": ^4.10.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/yup": ^0.29.13 ajv: ^8.10.0 chokidar: ^3.5.2 @@ -3851,7 +3851,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" - "@types/node": "*" + "@types/node": ^16.0.0 lodash: ^4.17.21 languageName: unknown linkType: soft @@ -3870,7 +3870,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/prop-types": ^15.7.3 "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 @@ -3968,7 +3968,7 @@ __metadata: "@types/d3-zoom": ^3.0.1 "@types/dagre": ^0.7.44 "@types/google-protobuf": ^3.7.2 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react-helmet": ^6.1.0 "@types/react-sparklines": ^1.7.0 "@types/react-syntax-highlighter": ^15.0.0 @@ -4025,7 +4025,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/prop-types": ^15.7.3 "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 @@ -4049,7 +4049,7 @@ __metadata: "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 "@types/inquirer": ^8.1.3 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/recursive-readdir": ^2.2.0 chalk: ^4.0.0 commander: ^9.1.0 @@ -4085,7 +4085,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.0.0 react-use: ^17.2.4 zen-observable: ^0.10.0 peerDependencies: @@ -4174,7 +4174,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4318,7 +4318,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4347,7 +4347,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4375,7 +4375,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-ga: ^3.3.0 @@ -4401,7 +4401,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.10.1 @@ -4445,7 +4445,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/swagger-ui-react": ^4.1.1 cross-fetch: ^3.1.5 graphiql: ^1.8.8 @@ -4656,7 +4656,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 humanize-duration: ^3.27.0 luxon: ^3.0.0 @@ -4718,7 +4718,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -4770,7 +4770,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -4862,7 +4862,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 lodash: ^4.17.21 @@ -5560,7 +5560,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.25.1 - "@types/node": "*" + "@types/node": ^16.11.26 circleci-api: ^4.0.0 cross-fetch: ^3.1.5 humanize-duration: ^3.27.0 @@ -5593,7 +5593,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -5625,7 +5625,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.27.1 "@types/luxon": ^3.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 humanize-duration: ^3.27.1 luxon: ^3.0.0 @@ -5687,7 +5687,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/highlightjs": ^10.1.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 highlight.js: ^10.6.0 @@ -5751,7 +5751,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 jsonschema: ^1.2.6 msw: ^0.49.0 @@ -5792,7 +5792,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/pluralize": ^0.0.29 "@types/recharts": ^1.8.14 "@types/regression": ^2.0.0 @@ -6017,7 +6017,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 languageName: unknown @@ -6048,7 +6048,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 classnames: ^2.2.6 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -6078,7 +6078,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6108,7 +6108,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6140,7 +6140,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/dompurify": ^2.3.3 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/sanitize-html": ^2.6.2 classnames: ^2.3.1 cross-fetch: ^3.1.5 @@ -6173,7 +6173,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 peerDependencies: @@ -6202,7 +6202,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6238,7 +6238,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6272,7 +6272,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -6336,7 +6336,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6366,7 +6366,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -6398,7 +6398,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/lodash": ^4.14.173 "@types/luxon": ^3.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 @@ -6428,7 +6428,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/codemirror": ^5.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 graphiql: ^1.5.12 graphql: ^16.0.0 @@ -6538,7 +6538,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^0.49.0 @@ -6572,7 +6572,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 humanize-duration: ^3.26.0 luxon: ^3.0.0 @@ -6640,7 +6640,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/testing-library__jest-dom": ^5.9.1 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -6696,7 +6696,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 jest-when: ^3.1.0 msw: ^0.49.0 @@ -6784,7 +6784,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cronstrue: ^2.2.0 cross-fetch: ^3.1.5 @@ -6846,7 +6846,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -6897,7 +6897,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -6955,7 +6955,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 p-limit: ^3.1.0 @@ -6988,7 +6988,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7042,7 +7042,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 msw: ^0.49.0 @@ -7290,7 +7290,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 @@ -7342,7 +7342,7 @@ __metadata: "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 "@types/mock-fs": ^4.13.0 - "@types/node": "*" + "@types/node": ^16.11.26 command-exists: ^1.2.9 fs-extra: ^10.0.1 jest-when: ^3.1.0 @@ -7556,7 +7556,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/humanize-duration": ^3.18.1 "@types/json-schema": ^7.0.9 - "@types/node": "*" + "@types/node": ^16.11.26 "@uiw/react-codemirror": ^4.9.3 classnames: ^2.2.6 cross-fetch: ^3.1.5 @@ -7761,7 +7761,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.9.4 @@ -7794,7 +7794,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7825,7 +7825,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/zen-observable": ^0.8.2 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -7893,7 +7893,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 rc-progress: 3.4.1 @@ -7923,7 +7923,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 @@ -7997,7 +7997,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^0.49.0 @@ -8109,7 +8109,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 qs: ^6.9.4 @@ -8140,7 +8140,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/color": ^3.0.1 "@types/d3-force": ^3.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 color: ^4.0.1 cross-fetch: ^3.1.5 @@ -8175,7 +8175,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -8243,7 +8243,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 git-url-parse: ^13.0.0 @@ -8356,7 +8356,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/dompurify": ^2.2.2 "@types/event-source-polyfill": ^1.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 canvas: ^2.10.2 cross-fetch: ^3.1.5 dompurify: ^2.2.9 @@ -8418,7 +8418,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 react-use: ^17.2.4 @@ -8471,7 +8471,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 @@ -8558,7 +8558,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/luxon": ^3.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 @@ -8576,7 +8576,7 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/test-utils": "workspace:^" - "@types/node": "*" + "@types/node": ^16.0.0 cross-fetch: ^3.1.5 msw: ^0.49.0 languageName: unknown @@ -8633,7 +8633,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 zen-observable: ^0.10.0 @@ -10546,7 +10546,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/test-utils": "workspace:^" - "@types/node": "*" + "@types/node": ^16.11.26 cross-fetch: ^3.1.5 msw: ^0.49.0 languageName: unknown @@ -10569,7 +10569,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: @@ -13491,7 +13491,7 @@ __metadata: "@types/dockerode": ^3.3.0 "@types/fs-extra": ^9.0.6 "@types/http-proxy": ^1.17.4 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/serve-handler": ^6.1.0 "@types/webpack-env": ^1.15.3 commander: ^9.1.0 @@ -14841,10 +14841,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^16.9.2": - version: 16.18.11 - resolution: "@types/node@npm:16.18.11" - checksum: 2a3b1da13063debe6e26f732defb5f03ef4ef732c3e08daba838d8850433bd00e537ce1a97ce9bcfc4b15db5218d701d1265fae94e0d6926906bec157e6b46e0 +"@types/node@npm:^16.0.0, @types/node@npm:^16.11.26, @types/node@npm:^16.9.2": + version: 16.18.12 + resolution: "@types/node@npm:16.18.12" + checksum: fc3271182414f8593018ef8f00b4718116a92f463f619081bd399d9460e7861e1dd7eebc7cf94c23567e418ff397babed077011711aae8d47171b5a81c5bd71d languageName: node linkType: hard @@ -20874,7 +20874,7 @@ __metadata: "@backstage/cli-common": "workspace:^" "@backstage/errors": "workspace:^" "@types/fs-extra": ^9.0.1 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/puppeteer": ^5.4.4 chalk: ^4.0.0 commander: ^9.1.0 @@ -22194,7 +22194,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/jquery": ^3.3.34 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react": "*" "@types/react-dom": "*" "@types/zen-observable": ^0.8.0 @@ -34251,7 +34251,7 @@ __metadata: "@spotify/eslint-plugin": ^14.1.3 "@spotify/prettier-config": ^14.0.0 "@techdocs/cli": "workspace:*" - "@types/node": "*" + "@types/node": ^16.11.26 "@types/webpack": ^5.28.0 command-exists: ^1.2.9 concurrently: ^7.0.0 @@ -36275,7 +36275,7 @@ __metadata: "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 - "@types/node": "*" + "@types/node": ^16.11.26 "@types/react-dom": "*" cross-env: ^7.0.0 cypress: ^10.0.0 From e7169461033c205f5554fb2f2598fa05b825fe1a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Feb 2023 15:54:19 +0100 Subject: [PATCH 73/90] backend-plugin-api: separate out hook in addShutdownHook Signed-off-by: Patrik Oldsberg --- .changeset/bright-plums-cough.md | 5 ++++ .changeset/green-guests-flow.md | 8 +++++ docs/backend-system/core-services/01-index.md | 29 +++++++++---------- .../lifecycle/lifecycleFactory.ts | 12 ++++---- .../rootHttpRouter/rootHttpRouterFactory.ts | 7 +---- .../rootLifecycleFactory.test.ts | 20 +++++++------ .../rootLifecycle/rootLifecycleFactory.ts | 19 ++++++++---- .../src/database/connectors/sqlite3.ts | 10 +++---- packages/backend-plugin-api/api-report.md | 13 ++++++--- .../services/definitions/LifecycleService.ts | 14 ++++++--- .../src/services/definitions/index.ts | 1 + .../src/next/wiring/TestBackend.test.ts | 8 ++--- .../src/next/wiring/TestBackend.ts | 7 +---- .../src/service/CatalogPlugin.ts | 6 +--- 14 files changed, 88 insertions(+), 71 deletions(-) create mode 100644 .changeset/bright-plums-cough.md create mode 100644 .changeset/green-guests-flow.md diff --git a/.changeset/bright-plums-cough.md b/.changeset/bright-plums-cough.md new file mode 100644 index 0000000000..9af30f002e --- /dev/null +++ b/.changeset/bright-plums-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING**: Split out the hook for both lifecycle services so that the first parameter of `addShutdownHook` is the hook function, and the second is the options. diff --git a/.changeset/green-guests-flow.md b/.changeset/green-guests-flow.md new file mode 100644 index 0000000000..688ff17dd9 --- /dev/null +++ b/.changeset/green-guests-flow.md @@ -0,0 +1,8 @@ +--- +'@backstage/backend-test-utils': patch +'@backstage/backend-app-api': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +--- + +Updated usage of the lifecycle service. diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index b394391480..711357df61 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -479,10 +479,7 @@ createBackendPlugin({ // do some other stuff. }, 1000); - lifecycle.addShutdownHook({ - fn: () => clearInterval(interval), - logger, - }); + lifecycle.addShutdownHook(() => clearInterval(interval)); }, }); }, @@ -499,17 +496,19 @@ The following example shows how to override the default implementation of the li ```ts class MyCustomLifecycleService implements RootLifecycleService { - constructor(private readonly logger: LoggerService) { - ['SIGKILL', 'SIGTERM'].map(signal => - process.on(signal, () => this.shutdown()), - ); - } + constructor(private readonly logger: LoggerService) {} #isCalled = false; - #shutdownTasks: Array = []; + #shutdownTasks: Array<{ + hook: LifecycleServiceShutdownHook; + options?: LifecycleServiceShutdownOptions; + }> = []; - addShutdownHook(options: LifecycleServiceShutdownHook): void { - this.#shutdownTasks.push(options); + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + this.#shutdownTasks.push({ hook, options }); } async shutdown(): Promise { @@ -520,10 +519,10 @@ class MyCustomLifecycleService implements RootLifecycleService { this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( - this.#shutdownTasks.map(async hook => { - const { logger = this.logger } = hook; + this.#shutdownTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; try { - await hook.fn(); + await hook(); logger.info(`Shutdown hook succeeded`); } catch (error) { logger.error(`Shutdown hook failed, ${error}`); diff --git a/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleFactory.ts b/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleFactory.ts index 299022079b..27c1a139fd 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleFactory.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleFactory.ts @@ -17,6 +17,7 @@ import { createServiceFactory, coreServices, LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, } from '@backstage/backend-plugin-api'; /** @@ -33,11 +34,12 @@ export const lifecycleFactory = createServiceFactory({ async factory({ rootLifecycle, logger, pluginMetadata }) { const plugin = pluginMetadata.getId(); return { - addShutdownHook(options: LifecycleServiceShutdownHook): void { - rootLifecycle.addShutdownHook({ - ...options, - - logger: options.logger?.child({ plugin }) ?? logger, + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + rootLifecycle.addShutdownHook(hook, { + logger: options?.logger?.child({ plugin }) ?? logger, }); }, }; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts index 58f8cc2db1..f46f92741b 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts @@ -104,12 +104,7 @@ export const rootHttpRouterFactory = createServiceFactory({ { logger }, ); - lifecycle.addShutdownHook({ - async fn() { - await server.stop(); - }, - logger, - }); + lifecycle.addShutdownHook(() => server.stop()); await server.start(); diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.test.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.test.ts index fc1c266775..3a93cacc8d 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.test.ts @@ -21,11 +21,7 @@ describe('lifecycleService', () => { it('should execute registered shutdown hook', async () => { const service = new BackendLifecycleImpl(getVoidLogger()); const hook = jest.fn(); - service.addShutdownHook({ - fn: async () => { - hook(); - }, - }); + service.addShutdownHook(() => hook()); // should not execute the hook more than once. await service.shutdown(); await service.shutdown(); @@ -35,10 +31,16 @@ describe('lifecycleService', () => { it('should not throw errors', async () => { const service = new BackendLifecycleImpl(getVoidLogger()); - service.addShutdownHook({ - fn: async () => { - throw new Error('oh no'); - }, + service.addShutdownHook(() => { + throw new Error('oh no'); + }); + await expect(service.shutdown()).resolves.toBeUndefined(); + }); + + it('should not throw async errors', async () => { + const service = new BackendLifecycleImpl(getVoidLogger()); + service.addShutdownHook(async () => { + throw new Error('oh no'); }); await expect(service.shutdown()).resolves.toBeUndefined(); }); diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts index ebff53e9a6..2b6f7eeafe 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts @@ -18,6 +18,7 @@ import { createServiceFactory, coreServices, LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, RootLifecycleService, LoggerService, } from '@backstage/backend-plugin-api'; @@ -26,10 +27,16 @@ export class BackendLifecycleImpl implements RootLifecycleService { constructor(private readonly logger: LoggerService) {} #isCalled = false; - #shutdownTasks: Array = []; + #shutdownTasks: Array<{ + hook: LifecycleServiceShutdownHook; + options?: LifecycleServiceShutdownOptions; + }> = []; - addShutdownHook(options: LifecycleServiceShutdownHook): void { - this.#shutdownTasks.push(options); + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + this.#shutdownTasks.push({ hook, options }); } async shutdown(): Promise { @@ -40,10 +47,10 @@ export class BackendLifecycleImpl implements RootLifecycleService { this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( - this.#shutdownTasks.map(async hook => { - const { logger = this.logger } = hook; + this.#shutdownTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; try { - await hook.fn(); + await hook(); logger.info(`Shutdown hook succeeded`); } catch (error) { logger.error(`Shutdown hook failed, ${error}`); diff --git a/packages/backend-common/src/database/connectors/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts index e1ed6b18e9..3cb0f57de6 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -81,12 +81,10 @@ export function createSqliteDatabaseClient( }); // If the dev store is available we save the database state on shutdown - deps.lifecycle.addShutdownHook({ - async fn() { - const connection = await database.client.acquireConnection(); - const data = connection.serialize(); - await devStore.save(dataKey, data); - }, + deps.lifecycle.addShutdownHook(async () => { + const connection = await database.client.acquireConnection(); + const data = connection.serialize(); + await devStore.save(dataKey, data); }); } else { database = knexFactory(knexConfig); diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index a7700d81ac..1add61348c 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -294,14 +294,19 @@ export interface IdentityService extends IdentityApi {} // @public (undocumented) export interface LifecycleService { - addShutdownHook(options: LifecycleServiceShutdownHook): void; + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void; } // @public (undocumented) -export type LifecycleServiceShutdownHook = { - fn: () => void | Promise; +export type LifecycleServiceShutdownHook = () => void | Promise; + +// @public (undocumented) +export interface LifecycleServiceShutdownOptions { logger?: LoggerService; -}; +} // @public export interface LoggerService { diff --git a/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts b/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts index 59d14aa61d..89351194e6 100644 --- a/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts +++ b/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts @@ -19,14 +19,17 @@ import { LoggerService } from './LoggerService'; /** * @public */ -export type LifecycleServiceShutdownHook = { - fn: () => void | Promise; +export type LifecycleServiceShutdownHook = () => void | Promise; +/** + * @public + */ +export interface LifecycleServiceShutdownOptions { /** * Optional {@link LoggerService} that will be used for logging instead of the default logger. */ logger?: LoggerService; -}; +} /** * @public @@ -35,5 +38,8 @@ export interface LifecycleService { /** * Register a function to be called when the backend is shutting down. */ - addShutdownHook(options: LifecycleServiceShutdownHook): void; + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void; } diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index c5af0967bd..720b4d634d 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -28,6 +28,7 @@ export type { HttpRouterService } from './HttpRouterService'; export type { LifecycleService, LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, } from './LifecycleService'; export type { LoggerService, LogMeta } from './LoggerService'; export type { PermissionsService } from './PermissionsService'; diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts index dca463a9ae..9bda3f3f5b 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -40,10 +40,8 @@ beforeAll(async () => { env.registerInit({ deps: { lifecycle: coreServices.lifecycle }, async init({ lifecycle }) { - lifecycle.addShutdownHook({ - fn() { - globalTestBackendHasBeenStopped = true; - }, + lifecycle.addShutdownHook(() => { + globalTestBackendHasBeenStopped = true; }); }, }); @@ -145,7 +143,7 @@ describe('TestBackend', () => { lifecycle: coreServices.lifecycle, }, async init({ lifecycle }) { - lifecycle.addShutdownHook({ fn: shutdownSpy }); + lifecycle.addShutdownHook(shutdownSpy); }, }); }, diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index db1016fcb6..27a8c5bceb 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -129,12 +129,7 @@ export async function startTestBackend< { logger }, ); - lifecycle.addShutdownHook({ - async fn() { - await server.stop(); - }, - logger, - }); + lifecycle.addShutdownHook(() => server.stop(), { logger }); await server.start(); diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index effed7f8f1..a726d3261b 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -97,11 +97,7 @@ export const catalogPlugin = createBackendPlugin({ const { processingEngine, router } = await builder.build(); await processingEngine.start(); - lifecycle.addShutdownHook({ - fn: async () => { - await processingEngine.stop(); - }, - }); + lifecycle.addShutdownHook(() => processingEngine.stop()); httpRouter.use(router); }, }); From 000fd3f4a8a57311c5301d0de0fd6621f50fe607 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Feb 2023 17:24:10 +0100 Subject: [PATCH 74/90] eslint-plugin: fix no-undeclared-imports test on windows Signed-off-by: Patrik Oldsberg --- .../src/no-undeclared-imports.test.ts | 142 ++++++++++++------ 1 file changed, 94 insertions(+), 48 deletions(-) diff --git a/packages/eslint-plugin/src/no-undeclared-imports.test.ts b/packages/eslint-plugin/src/no-undeclared-imports.test.ts index c156558e8d..1b5cbee853 100644 --- a/packages/eslint-plugin/src/no-undeclared-imports.test.ts +++ b/packages/eslint-plugin/src/no-undeclared-imports.test.ts @@ -15,11 +15,11 @@ */ import { RuleTester } from 'eslint'; -import path from 'path'; +import { join as joinPath } from 'path'; import rule from '../rules/no-undeclared-imports'; const RULE = 'no-undeclared-imports'; -const FIXTURE = path.resolve(__dirname, '__fixtures__/monorepo'); +const FIXTURE = joinPath(__dirname, '__fixtures__/monorepo'); const ERR_UNDECLARED = ( name: string, @@ -27,7 +27,10 @@ const ERR_UNDECLARED = ( path: string, flag?: string, ) => ({ - message: `${name} must be declared in ${field} of ${path}/package.json, run 'yarn --cwd ${path} add${ + message: `${name} must be declared in ${field} of ${joinPath( + path, + 'package.json', + )}, run 'yarn --cwd ${path} add${ flag ? ` ${flag}` : '' } ${name}' from the project root.`, }); @@ -37,7 +40,10 @@ const ERR_SWITCHED = ( field: string, path: string, ) => ({ - message: `${name} is declared in ${old}, but should be moved to ${field} in ${path}/package.json.`, + message: `${name} is declared in ${old}, but should be moved to ${field} in ${joinPath( + path, + 'package.json', + )}.`, }); process.chdir(FIXTURE); @@ -53,141 +59,181 @@ ruleTester.run(RULE, rule, { valid: [ { code: `import '@internal/foo'`, - filename: path.join(FIXTURE, 'packages/foo/src/index.ts'), + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), }, { code: `import '@internal/bar'`, - filename: path.join(FIXTURE, 'packages/foo/src/index.ts'), + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), }, { code: `import 'react'`, - filename: path.join(FIXTURE, 'packages/foo/src/index.ts'), + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), }, { code: `import '@internal/foo'`, - filename: path.join(FIXTURE, 'packages/foo/src/index.test.ts'), + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), }, { code: `import '@internal/bar'`, - filename: path.join(FIXTURE, 'packages/foo/src/index.test.ts'), + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), }, { code: `import 'lodash'`, - filename: path.join(FIXTURE, 'packages/foo/src/index.test.ts'), + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), }, { code: `import 'react'`, - filename: path.join(FIXTURE, 'packages/foo/src/index.test.ts'), + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), }, { // We're only able to validate literals code: `require('lod' + 'ash')`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), }, ], invalid: [ { code: `import 'lodash'`, - filename: path.join(FIXTURE, 'packages/foo/src/index.ts'), + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), errors: [ ERR_SWITCHED( 'lodash', 'devDependencies', 'dependencies', - 'packages/foo', + joinPath('packages', 'foo'), ), ], }, { code: `import 'react-router'`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), errors: [ ERR_SWITCHED( 'react-router', 'dependencies', 'peerDependencies', - 'packages/bar', + joinPath('packages', 'bar'), ), ], }, { code: `import 'react-router-dom'`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), errors: [ ERR_SWITCHED( 'react-router-dom', 'devDependencies', 'peerDependencies', - 'packages/bar', + joinPath('packages', 'bar'), ), ], }, { code: `import 'lodash'`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), - errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], }, { code: `import { debounce } from 'lodash'`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), - errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], }, { code: `import * as _ from 'lodash'`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), - errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], }, { code: `import _ from 'lodash'`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), - errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], }, { code: `import('lodash')`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), - errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], }, { code: `require('lodash')`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), - errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], - }, - { - code: `import 'lodash'`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), - errors: [ERR_UNDECLARED('lodash', 'dependencies', 'packages/bar')], - }, - { - code: `import 'lodash'`, - filename: path.join(FIXTURE, 'packages/bar/src/index.test.ts'), + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), errors: [ - ERR_UNDECLARED('lodash', 'devDependencies', 'packages/bar', '--dev'), + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.test.ts'), + errors: [ + ERR_UNDECLARED( + 'lodash', + 'devDependencies', + joinPath('packages', 'bar'), + '--dev', + ), ], }, { code: `import 'react'`, - filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), errors: [ - ERR_UNDECLARED('react', 'peerDependencies', 'packages/bar', '--peer'), + ERR_UNDECLARED( + 'react', + 'peerDependencies', + joinPath('packages', 'bar'), + '--peer', + ), ], }, { code: `import 'react'`, - filename: path.join(FIXTURE, 'packages/bar/src/index.test.ts'), + filename: joinPath(FIXTURE, 'packages/bar/src/index.test.ts'), errors: [ - ERR_UNDECLARED('react', 'peerDependencies', 'packages/bar', '--peer'), + ERR_UNDECLARED( + 'react', + 'peerDependencies', + joinPath('packages', 'bar'), + '--peer', + ), ], }, { code: `import 'react-dom'`, - filename: path.join(FIXTURE, 'packages/foo/src/index.ts'), - errors: [ERR_UNDECLARED('react-dom', 'dependencies', 'packages/foo')], + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + errors: [ + ERR_UNDECLARED( + 'react-dom', + 'dependencies', + joinPath('packages', 'foo'), + ), + ], }, { code: `import 'react-dom'`, - filename: path.join(FIXTURE, 'packages/foo/src/index.test.ts'), + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), errors: [ - ERR_UNDECLARED('react-dom', 'devDependencies', 'packages/foo', '--dev'), + ERR_UNDECLARED( + 'react-dom', + 'devDependencies', + joinPath('packages', 'foo'), + '--dev', + ), ], }, ], From f1adb2e36b460b5c996504d1a6ed897d04968c4d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Feb 2023 17:34:26 +0100 Subject: [PATCH 75/90] backend-test-utils: remove mock service refs Signed-off-by: Patrik Oldsberg --- .changeset/thin-donkeys-attack.md | 5 ++++ packages/backend-test-utils/api-report.md | 27 ------------------- .../src/next/services/mockServices.ts | 24 +++++------------ 3 files changed, 12 insertions(+), 44 deletions(-) create mode 100644 .changeset/thin-donkeys-attack.md diff --git a/.changeset/thin-donkeys-attack.md b/.changeset/thin-donkeys-attack.md new file mode 100644 index 0000000000..2fd0aebb6a --- /dev/null +++ b/.changeset/thin-donkeys-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Removed the `ref` from all `mockServices`. diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 232f7e8136..6e0edde88b 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -20,7 +20,6 @@ import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; -import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; @@ -34,8 +33,6 @@ export function isDockerDisabledForTests(): boolean; export namespace mockServices { // (undocumented) export namespace cache { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: () => ServiceFactory; } @@ -47,22 +44,16 @@ export namespace mockServices { export type Options = { data?: JsonObject; }; - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: (options?: Options | undefined) => ServiceFactory; } // (undocumented) export namespace database { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: () => ServiceFactory; } // (undocumented) export namespace httpRouter { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: ( options?: HttpRouterFactoryOptions | undefined, @@ -72,36 +63,26 @@ export namespace mockServices { export function identity(): IdentityService; // (undocumented) export namespace identity { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: () => ServiceFactory; } // (undocumented) export namespace lifecycle { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: () => ServiceFactory; } // (undocumented) export namespace logger { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: () => ServiceFactory; } // (undocumented) export namespace permissions { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: () => ServiceFactory; } // (undocumented) export namespace rootLifecycle { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: () => ServiceFactory; } @@ -120,15 +101,11 @@ export namespace mockServices { debug: boolean; }; }; - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: (options?: Options | undefined) => ServiceFactory; } // (undocumented) export namespace scheduler { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: () => ServiceFactory; } @@ -136,15 +113,11 @@ export namespace mockServices { export function tokenManager(): TokenManagerService; // (undocumented) export namespace tokenManager { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: () => ServiceFactory; } // (undocumented) export namespace urlReader { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: () => ServiceFactory; } diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index f86d8bac55..ef32993611 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -62,8 +62,7 @@ export namespace mockServices { export namespace config { export type Options = { data?: JsonObject }; - export const ref = coreServices.config; - export const factory = simpleFactory(ref, config); + export const factory = simpleFactory(coreServices.config, config); } export function rootLogger(options?: rootLogger.Options): LoggerService { @@ -76,8 +75,7 @@ export namespace mockServices { | { error: boolean; warn: boolean; info: boolean; debug: boolean }; }; - export const ref = coreServices.rootLogger; - export const factory = simpleFactory(ref, rootLogger); + export const factory = simpleFactory(coreServices.rootLogger, rootLogger); } export function tokenManager(): TokenManagerService { @@ -93,55 +91,47 @@ export namespace mockServices { }; } export namespace tokenManager { - export const ref = coreServices.tokenManager; - export const factory = simpleFactory(ref, tokenManager); + export const factory = simpleFactory( + coreServices.tokenManager, + tokenManager, + ); } export function identity(): IdentityService { return new MockIdentityService(); } export namespace identity { - export const ref = coreServices.identity; - export const factory = simpleFactory(ref, identity); + export const factory = simpleFactory(coreServices.identity, identity); } // TODO(Rugvip): Not all core services have implementations available here yet. // some may need a bit more refactoring for it to be simpler to // re-implement functioning mock versions here. export namespace cache { - export const ref = coreServices.cache; export const factory = cacheFactory; } export namespace database { - export const ref = coreServices.database; export const factory = databaseFactory; } export namespace httpRouter { - export const ref = coreServices.httpRouter; export const factory = httpRouterFactory; } export namespace lifecycle { - export const ref = coreServices.lifecycle; export const factory = lifecycleFactory; } export namespace logger { - export const ref = coreServices.logger; export const factory = loggerFactory; } export namespace permissions { - export const ref = coreServices.permissions; export const factory = permissionsFactory; } export namespace rootLifecycle { - export const ref = coreServices.rootLifecycle; export const factory = rootLifecycleFactory; } export namespace scheduler { - export const ref = coreServices.scheduler; export const factory = schedulerFactory; } export namespace urlReader { - export const ref = coreServices.urlReader; export const factory = urlReaderFactory; } } From 1835311713c3ee767376250f4e5252d06a554c67 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Feb 2023 17:36:22 +0100 Subject: [PATCH 76/90] backend-test-utils: explicit return type for mockServices.config() Signed-off-by: Patrik Oldsberg --- .changeset/four-tables-fix.md | 5 +++++ packages/backend-test-utils/api-report.md | 3 +-- .../backend-test-utils/src/next/services/mockServices.ts | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/four-tables-fix.md diff --git a/.changeset/four-tables-fix.md b/.changeset/four-tables-fix.md new file mode 100644 index 0000000000..f17f57e253 --- /dev/null +++ b/.changeset/four-tables-fix.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Added explicit return type signature for `mockServices.config()`. diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 6e0edde88b..672c574cb2 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -6,7 +6,6 @@ import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { CacheService } from '@backstage/backend-plugin-api'; -import { ConfigReader } from '@backstage/config'; import { ConfigService } from '@backstage/backend-plugin-api'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { ExtendedHttpServer } from '@backstage/backend-app-api'; @@ -37,7 +36,7 @@ export namespace mockServices { factory: () => ServiceFactory; } // (undocumented) - export function config(options?: config.Options): ConfigReader; + export function config(options?: config.Options): ConfigService; // (undocumented) export namespace config { // (undocumented) diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index ef32993611..5f771f13c5 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -15,6 +15,7 @@ */ import { + ConfigService, coreServices, createServiceFactory, IdentityService, @@ -56,7 +57,7 @@ function simpleFactory( * @alpha */ export namespace mockServices { - export function config(options?: config.Options) { + export function config(options?: config.Options): ConfigService { return new ConfigReader(options?.data, 'mock-config'); } export namespace config { From 1d4692fb3ddfcfdd31181d41e6e3def90b06f052 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 16:42:10 +0000 Subject: [PATCH 77/90] fix(deps): update dependency @esbuild-kit/cjs-loader to v2.4.2 Signed-off-by: Renovate Bot --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 83503fb359..ecb0826a83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9212,12 +9212,12 @@ __metadata: linkType: hard "@esbuild-kit/cjs-loader@npm:^2.4.1": - version: 2.4.1 - resolution: "@esbuild-kit/cjs-loader@npm:2.4.1" + version: 2.4.2 + resolution: "@esbuild-kit/cjs-loader@npm:2.4.2" dependencies: "@esbuild-kit/core-utils": ^3.0.0 - get-tsconfig: ^4.2.0 - checksum: a516065907be0ead76ac2199ccb08ff92659ba5e2edb4bb8772b6a63afe4faed7eb45a3b4d87266a68c7c135c3dba971cd087bc6f16c382356e835c7dd3440f5 + get-tsconfig: ^4.4.0 + checksum: e346e339bfc7eff5c52c270fd0ec06a7f2341b624adfb69f84b7d83f119c35070420906f2761a0b4604e0a0ec90e35eaf12544585476c428ed6d6ee3b250c0fe languageName: node linkType: hard @@ -23525,10 +23525,10 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.2.0": - version: 4.3.0 - resolution: "get-tsconfig@npm:4.3.0" - checksum: 2597aab99aa3a24db209e192a3e5874ac47fc5abc71703ee26346e0c5816cb346ca09fc813c739db5862d3a2905d89aeca1b0cbc46c2b272398d672309aaf414 +"get-tsconfig@npm:^4.4.0": + version: 4.4.0 + resolution: "get-tsconfig@npm:4.4.0" + checksum: e193558b4f0c84c81ae9688cf5b9950dc0b341e44f91b002713fd0c37cfb73108e1cd9998ed540bcc423f193fde32cc58a15e99dd469f5158a2eb4a148611176 languageName: node linkType: hard From a1e4a3e842f76e37e19ad7a76c44f8594e88975f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 16:42:41 +0000 Subject: [PATCH 78/90] fix(deps): update dependency @google-cloud/storage to v6.9.2 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 83503fb359..c3c0faab14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9602,8 +9602,8 @@ __metadata: linkType: hard "@google-cloud/storage@npm:^6.0.0": - version: 6.9.1 - resolution: "@google-cloud/storage@npm:6.9.1" + version: 6.9.2 + resolution: "@google-cloud/storage@npm:6.9.2" dependencies: "@google-cloud/paginator": ^3.0.7 "@google-cloud/projectify": ^3.0.0 @@ -9622,7 +9622,7 @@ __metadata: retry-request: ^5.0.0 teeny-request: ^8.0.0 uuid: ^8.0.0 - checksum: 9967bc0e80d1531f91030d27fc46cef0242f70dde2064b051104d8d261f1d3cba38b51789b0ae0850bf99b13d093365f85c85c43a2d9468d81f0f98238f76e78 + checksum: e4962be29ca3dd722019db95f99b095993534915d663a82b41c95beaf284d878de5b1357102441e4e375de838299f6d82eff55a2c4e9f610f9a12feac21e2735 languageName: node linkType: hard From 0ff03319be02103dea4bcb36ffcfeba272c31af6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Feb 2023 17:49:57 +0100 Subject: [PATCH 79/90] backend-plugin-api: switch createBackendPlugin id -> pluginId Signed-off-by: Patrik Oldsberg --- .changeset/bright-teachers-compete.md | 5 ++++ .changeset/little-jars-lay.md | 18 ++++++++++++++ .../backend-system/architecture/04-plugins.md | 4 ++-- .../architecture/05-extension-points.md | 2 +- .../architecture/07-naming-patterns.md | 4 ++-- .../building-plugins-and-modules/01-index.md | 6 ++--- .../08-migrating.md | 6 ++--- docs/backend-system/core-services/01-index.md | 24 +++++++++---------- .../02-adding-a-basic-permission-check.md | 2 +- docs/plugins/new-backend-system.md | 4 ++-- .../scheduler/schedulerFactory.test.ts | 2 +- packages/backend-common/src/legacy.ts | 2 +- .../src/CreateBackend.test.ts | 2 +- packages/backend-plugin-api/api-report.md | 2 +- .../src/wiring/factories.test.ts | 10 ++++---- .../src/wiring/factories.ts | 16 +++++++++---- .../src/next/wiring/TestBackend.test.ts | 4 ++-- plugins/app-backend/src/service/appPlugin.ts | 2 +- plugins/bazaar-backend/src/plugin.ts | 2 +- .../src/service/CatalogPlugin.ts | 2 +- .../src/service/EventsPlugin.ts | 2 +- .../example-todo-list-backend/src/plugin.ts | 2 +- plugins/kafka-backend/src/plugin.ts | 2 +- plugins/periskop-backend/src/plugin.ts | 2 +- plugins/proxy-backend/src/plugin.ts | 2 +- .../src/ScaffolderPlugin.ts | 2 +- plugins/user-settings-backend/src/plugin.ts | 2 +- 27 files changed, 82 insertions(+), 51 deletions(-) create mode 100644 .changeset/bright-teachers-compete.md create mode 100644 .changeset/little-jars-lay.md diff --git a/.changeset/bright-teachers-compete.md b/.changeset/bright-teachers-compete.md new file mode 100644 index 0000000000..3cc900d613 --- /dev/null +++ b/.changeset/bright-teachers-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING**: The plugin ID option passed to `createBackendPlugin` is now `pluginId`, rather than just `id`. This is to make it match `createBackendModule` more closely. diff --git a/.changeset/little-jars-lay.md b/.changeset/little-jars-lay.md new file mode 100644 index 0000000000..161dc099e9 --- /dev/null +++ b/.changeset/little-jars-lay.md @@ -0,0 +1,18 @@ +--- +'@internal/plugin-todo-list-backend': patch +'@backstage/plugin-user-settings-backend': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/backend-defaults': patch +'@backstage/backend-app-api': patch +'@backstage/plugin-periskop-backend': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-events-backend': patch +'@backstage/plugin-kafka-backend': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-app-backend': patch +--- + +Updated usage of `createBackendPlugin`. diff --git a/docs/backend-system/architecture/04-plugins.md b/docs/backend-system/architecture/04-plugins.md index 7721b1c9f7..e73f086c43 100644 --- a/docs/backend-system/architecture/04-plugins.md +++ b/docs/backend-system/architecture/04-plugins.md @@ -22,7 +22,7 @@ import { } from '@backstage/backend-plugin-api'; export const examplePlugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -53,7 +53,7 @@ export interface ExamplePluginOptions { export const examplePlugin = createBackendPlugin( (options?: ExamplePluginOptions) => ({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { diff --git a/docs/backend-system/architecture/05-extension-points.md b/docs/backend-system/architecture/05-extension-points.md index d16a773c0c..278c565472 100644 --- a/docs/backend-system/architecture/05-extension-points.md +++ b/docs/backend-system/architecture/05-extension-points.md @@ -44,7 +44,7 @@ class ActionsExtension implements ScaffolderActionsExtensionPoint { export const scaffolderPlugin = createBackendPlugin( { - id: 'scaffolder', + pluginId: 'scaffolder', register(env) { const actionsExtensions = new ActionsExtension(); env.registerExtensionPoint( diff --git a/docs/backend-system/architecture/07-naming-patterns.md b/docs/backend-system/architecture/07-naming-patterns.md index dc781a61ac..b4b4c6c70f 100644 --- a/docs/backend-system/architecture/07-naming-patterns.md +++ b/docs/backend-system/architecture/07-naming-patterns.md @@ -21,7 +21,7 @@ Example: ```ts export const catalogPlugin = createBackendPlugin({ - id: 'catalog', + pluginId: 'catalog', ... }) ``` @@ -36,7 +36,7 @@ export const catalogPlugin = createBackendPlugin({ Example: ```ts -export const catalogModuleGithubEntityProvider = createBackendPlugin({ +export const catalogModuleGithubEntityProvider = createBackendModule({ pluginId: 'catalog', moduleId: 'githubEntityProvider', ... diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 5fefb3ff16..cae5052d04 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -36,7 +36,7 @@ import { import { createExampleRouter } from './router'; export const examplePlugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -183,7 +183,7 @@ that needs to be different across environments. import { coreServices } from '@backstage/backend-plugin-api'; export const examplePlugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { config: coreServices.config }, @@ -216,7 +216,7 @@ export interface ExampleOptions { export const examplePlugin = createBackendPlugin( (options?: ExampleOptions) => ({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { diff --git a/docs/backend-system/building-plugins-and-modules/08-migrating.md b/docs/backend-system/building-plugins-and-modules/08-migrating.md index ec5836a126..687f4c1d5e 100644 --- a/docs/backend-system/building-plugins-and-modules/08-migrating.md +++ b/docs/backend-system/building-plugins-and-modules/08-migrating.md @@ -46,7 +46,7 @@ import { Router } from 'express'; import { KubernetesBuilder } from './KubernetesBuilder'; export const kubernetesPlugin = createBackendPlugin({ - id: 'kubernetes', + pluginId: 'kubernetes', register(env) { env.registerInit({ deps: { @@ -95,7 +95,7 @@ export interface KubernetesOptions { } const kubernetesPlugin = createBackendPlugin((options: KubernetesOptions) => ({ - id: 'kubernetes', + pluginId: 'kubernetes', register(env) { env.registerInit({ deps: { @@ -182,7 +182,7 @@ class ClusterSupplier implements KubernetesClusterSupplierExtensionPoint { } export const kubernetesPlugin = createBackendPlugin({ - id: 'kubernetes', + pluginId: 'kubernetes', register(env) { const extensionPoint = new ClusterSupplier(); // We register the extension point with the backend, which allows modules to diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index b394391480..18c7b1079f 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -33,7 +33,7 @@ import { import { Router } from 'express'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { http: coreServices.httpRouter }, @@ -86,7 +86,7 @@ import { import { Router } from 'express'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -157,7 +157,7 @@ import { } from '@backstage/backend-plugin-api'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -215,7 +215,7 @@ import { } from '@backstage/backend-plugin-api'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -288,7 +288,7 @@ import { } from '@backstage/backend-plugin-api'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -327,7 +327,7 @@ import { import { resolvePackagePath } from '@backstage/backend-common'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -366,7 +366,7 @@ import { import { fetch } from 'node-fetch'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -397,7 +397,7 @@ import { import { Router } from 'express'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -465,7 +465,7 @@ import { } from '@backstage/backend-plugin-api'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -564,7 +564,7 @@ import { import { Router } from 'express'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -613,7 +613,7 @@ import { import { fetch } from 'node-fetch'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -652,7 +652,7 @@ import { import os from 'os'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 551e48f91a..2ceb8057f4 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -301,7 +301,7 @@ Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`: * @alpha */ export const exampleTodoListPlugin = createBackendPlugin({ - id: 'exampleTodoList', + pluginId: 'exampleTodoList', register(env) { env.registerInit({ deps: { diff --git a/docs/plugins/new-backend-system.md b/docs/plugins/new-backend-system.md index bfc6f79661..785d364ae2 100644 --- a/docs/plugins/new-backend-system.md +++ b/docs/plugins/new-backend-system.md @@ -84,7 +84,7 @@ import { // export type ExamplePluginOptions = { exampleOption: boolean }; export const examplePlugin = createBackendPlugin({ // unique id for the plugin - id: 'example', + pluginId: 'example', // It's possible to provide options to the plugin // register(env, options: ExamplePluginOptions) { register(env) { @@ -111,7 +111,7 @@ If we wanted our plugin to accept options as well, we'd accept the options as th ```ts export const examplePlugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env, options?: { silent?: boolean }) { env.registerInit({ deps: { logger: coreServices.logger }, diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.test.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.test.ts index 1aaf4ec71d..15ec364ac5 100644 --- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.test.ts @@ -28,7 +28,7 @@ describe('schedulerFactory', () => { const subject = schedulerFactory(); const plugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(reg) { reg.registerInit({ deps: { diff --git a/packages/backend-common/src/legacy.ts b/packages/backend-common/src/legacy.ts index 30e7dd9dc5..214e65e0c7 100644 --- a/packages/backend-common/src/legacy.ts +++ b/packages/backend-common/src/legacy.ts @@ -60,7 +60,7 @@ export function makeLegacyPlugin< }>, ) => { const compatPlugin = createBackendPlugin({ - id: name, + pluginId: name, register(env) { env.registerInit({ deps: { ...envMapping, _router: coreServices.httpRouter }, diff --git a/packages/backend-defaults/src/CreateBackend.test.ts b/packages/backend-defaults/src/CreateBackend.test.ts index 2cfb92c3a6..8d56cd0986 100644 --- a/packages/backend-defaults/src/CreateBackend.test.ts +++ b/packages/backend-defaults/src/CreateBackend.test.ts @@ -143,7 +143,7 @@ describe('createBackend', () => { expect.assertions(3); backend.add( createBackendPlugin({ - id: 'test', + pluginId: 'test', register(reg) { reg.registerInit({ deps: { diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index a7700d81ac..9a6f05babe 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -47,7 +47,7 @@ export interface BackendModuleRegistrationPoints { // @public export interface BackendPluginConfig { - id: string; + pluginId: string; // (undocumented) register(reg: BackendPluginRegistrationPoints): void; } diff --git a/packages/backend-plugin-api/src/wiring/factories.test.ts b/packages/backend-plugin-api/src/wiring/factories.test.ts index dbdf0ead14..8e1b30ea53 100644 --- a/packages/backend-plugin-api/src/wiring/factories.test.ts +++ b/packages/backend-plugin-api/src/wiring/factories.test.ts @@ -33,7 +33,7 @@ describe('createExtensionPoint', () => { describe('createBackendPlugin', () => { it('should create a BackendPlugin', () => { const plugin = createBackendPlugin((_options: { a: string }) => ({ - id: 'x', + pluginId: 'x', register() {}, })); expect(plugin).toBeDefined(); @@ -47,7 +47,7 @@ describe('createBackendPlugin', () => { it('should create plugins with optional options', () => { const plugin = createBackendPlugin((_options?: { a: string }) => ({ - id: 'x', + pluginId: 'x', register() {}, })); expect(plugin).toBeDefined(); @@ -59,7 +59,7 @@ describe('createBackendPlugin', () => { it('should create plugins without options', () => { const plugin = createBackendPlugin({ - id: 'x', + pluginId: 'x', register() {}, }); expect(plugin).toBeDefined(); @@ -74,7 +74,7 @@ describe('createBackendPlugin', () => { a: string; } const plugin = createBackendPlugin((_options: TestOptions) => ({ - id: 'x', + pluginId: 'x', register() {}, })); expect(plugin).toBeDefined(); @@ -91,7 +91,7 @@ describe('createBackendPlugin', () => { a: string; } const plugin = createBackendPlugin((_options?: TestOptions) => ({ - id: 'x', + pluginId: 'x', register() {}, })); expect(plugin).toBeDefined(); diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts index d281335a8e..6f37efabfe 100644 --- a/packages/backend-plugin-api/src/wiring/factories.ts +++ b/packages/backend-plugin-api/src/wiring/factories.ts @@ -72,7 +72,7 @@ export interface BackendPluginConfig { * * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} */ - id: string; + pluginId: string; register(reg: BackendPluginRegistrationPoints): void; } @@ -87,10 +87,18 @@ export function createBackendPlugin( config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig), ): (...params: TOptions) => BackendFeature { if (typeof config === 'function') { - return config; + return (...options: TOptions) => { + const c = config(...options); + return { + ...c, + id: c.pluginId, + }; + }; } - - return () => config; + return () => ({ + ...config, + id: config.pluginId, + }); } /** diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts index dca463a9ae..eca003c70b 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -165,7 +165,7 @@ describe('TestBackend', () => { expect.assertions(2); const testPlugin = createBackendPlugin({ - id: 'test', + pluginId: 'test', register(env) { env.registerInit({ deps: { @@ -201,7 +201,7 @@ describe('TestBackend', () => { it('should allow making requests via supertest', async () => { const testPlugin = createBackendPlugin({ - id: 'test', + pluginId: 'test', register(env) { env.registerInit({ deps: { diff --git a/plugins/app-backend/src/service/appPlugin.ts b/plugins/app-backend/src/service/appPlugin.ts index b12a8d6877..a29f8fd5a8 100644 --- a/plugins/app-backend/src/service/appPlugin.ts +++ b/plugins/app-backend/src/service/appPlugin.ts @@ -71,7 +71,7 @@ export type AppPluginOptions = { * @alpha */ export const appPlugin = createBackendPlugin((options: AppPluginOptions) => ({ - id: 'app', + pluginId: 'app', register(env) { env.registerInit({ deps: { diff --git a/plugins/bazaar-backend/src/plugin.ts b/plugins/bazaar-backend/src/plugin.ts index 03491950e8..62622717c8 100644 --- a/plugins/bazaar-backend/src/plugin.ts +++ b/plugins/bazaar-backend/src/plugin.ts @@ -27,7 +27,7 @@ import { createRouter } from './service/router'; * @alpha */ export const bazaarPlugin = createBackendPlugin({ - id: 'bazaar', + pluginId: 'bazaar', register(env) { env.registerInit({ deps: { diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index effed7f8f1..aadaf99a10 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -56,7 +56,7 @@ class CatalogExtensionPointImpl implements CatalogProcessingExtensionPoint { * @alpha */ export const catalogPlugin = createBackendPlugin({ - id: 'catalog', + pluginId: 'catalog', register(env) { const processingExtensions = new CatalogExtensionPointImpl(); // plugins depending on this API will be initialized before this plugins init method is executed. diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index 29456d71f7..402104b56c 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -80,7 +80,7 @@ class EventsExtensionPointImpl implements EventsExtensionPoint { * @alpha */ export const eventsPlugin = createBackendPlugin({ - id: 'events', + pluginId: 'events', register(env) { const extensionPoint = new EventsExtensionPointImpl(); env.registerExtensionPoint(eventsExtensionPoint, extensionPoint); diff --git a/plugins/example-todo-list-backend/src/plugin.ts b/plugins/example-todo-list-backend/src/plugin.ts index ba25ed7218..42985f6e88 100644 --- a/plugins/example-todo-list-backend/src/plugin.ts +++ b/plugins/example-todo-list-backend/src/plugin.ts @@ -27,7 +27,7 @@ import { createRouter } from './service/router'; * @alpha */ export const exampleTodoListPlugin = createBackendPlugin({ - id: 'exampleTodoList', + pluginId: 'exampleTodoList', register(env) { env.registerInit({ deps: { diff --git a/plugins/kafka-backend/src/plugin.ts b/plugins/kafka-backend/src/plugin.ts index f627e5a0bd..2442eddbaa 100644 --- a/plugins/kafka-backend/src/plugin.ts +++ b/plugins/kafka-backend/src/plugin.ts @@ -27,7 +27,7 @@ import { createRouter } from './service/router'; * @alpha */ export const kafkaPlugin = createBackendPlugin({ - id: 'kafka', + pluginId: 'kafka', register(env) { env.registerInit({ deps: { diff --git a/plugins/periskop-backend/src/plugin.ts b/plugins/periskop-backend/src/plugin.ts index b5dcfb1425..933c864d50 100644 --- a/plugins/periskop-backend/src/plugin.ts +++ b/plugins/periskop-backend/src/plugin.ts @@ -27,7 +27,7 @@ import { createRouter } from './service/router'; * @alpha */ export const periskopPlugin = createBackendPlugin({ - id: 'periskop', + pluginId: 'periskop', register(env) { env.registerInit({ deps: { diff --git a/plugins/proxy-backend/src/plugin.ts b/plugins/proxy-backend/src/plugin.ts index e9d6d515c1..d095bee527 100644 --- a/plugins/proxy-backend/src/plugin.ts +++ b/plugins/proxy-backend/src/plugin.ts @@ -28,7 +28,7 @@ import { createRouter } from './service/router'; */ export const proxyPlugin = createBackendPlugin( (options?: { skipInvalidProxies?: boolean }) => ({ - id: 'proxy', + pluginId: 'proxy', register(env) { env.registerInit({ deps: { diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 43b8d30f99..3b0d26be21 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -64,7 +64,7 @@ class ScaffolderActionsExtensionPointImpl */ export const scaffolderPlugin = createBackendPlugin( (options: ScaffolderPluginOptions) => ({ - id: 'scaffolder', + pluginId: 'scaffolder', register(env) { const actionsExtensions = new ScaffolderActionsExtensionPointImpl(); diff --git a/plugins/user-settings-backend/src/plugin.ts b/plugins/user-settings-backend/src/plugin.ts index 83120cc180..e200d22ffe 100644 --- a/plugins/user-settings-backend/src/plugin.ts +++ b/plugins/user-settings-backend/src/plugin.ts @@ -26,7 +26,7 @@ import { createRouter } from './service/router'; * @alpha */ export const userSettingsPlugin = createBackendPlugin({ - id: 'userSettings', + pluginId: 'userSettings', register(env) { env.registerInit({ deps: { From 181c03edb528d094ff15d5e637b60b27bace002b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Feb 2023 17:59:48 +0100 Subject: [PATCH 80/90] backend-plugin-api: align opaque type markers Signed-off-by: Patrik Oldsberg --- .changeset/itchy-chairs-punch.md | 5 +++++ packages/backend-plugin-api/api-report.md | 4 ++-- packages/backend-plugin-api/src/services/system/types.ts | 4 ++-- .../src/wiring/createSharedEnvironment.test.ts | 2 +- .../backend-plugin-api/src/wiring/createSharedEnvironment.ts | 4 ++-- packages/backend-plugin-api/src/wiring/factories.ts | 2 +- packages/backend-plugin-api/src/wiring/types.ts | 2 +- 7 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 .changeset/itchy-chairs-punch.md diff --git a/.changeset/itchy-chairs-punch.md b/.changeset/itchy-chairs-punch.md new file mode 100644 index 0000000000..034a9bbdae --- /dev/null +++ b/.changeset/itchy-chairs-punch.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Aligned opaque type markers to all use a `$$type` property with namespacing. diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index a7700d81ac..baa2b1b7dc 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -275,7 +275,7 @@ export type ExtensionPoint = { id: string; T: T; toString(): string; - $$ref: 'extension-point'; + $$type: '@backstage/ExtensionPoint'; }; // @public @@ -490,7 +490,7 @@ export type ServiceRef< scope: TScope; T: TService; toString(): string; - $$ref: 'service'; + $$type: '@backstage/ServiceRef'; }; // @public (undocumented) diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 2615c5e3a3..3e68e9349d 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -44,7 +44,7 @@ export type ServiceRef< toString(): string; - $$ref: 'service'; + $$type: '@backstage/ServiceRef'; }; /** @public */ @@ -119,7 +119,7 @@ export function createServiceRef( toString() { return `serviceRef{${config.id}}`; }, - $$ref: 'service', // TODO: declare + $$type: '@backstage/ServiceRef', __defaultFactory: defaultFactory, } as ServiceRef & { __defaultFactory?: ( diff --git a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts index ec2cf1fe49..381a4799d2 100644 --- a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts +++ b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts @@ -48,7 +48,7 @@ describe('createSharedEnvironment', () => { expect(env).toBeDefined(); const internalEnv = env() as unknown as InternalSharedBackendEnvironment; expect(internalEnv).toEqual({ - $$type: 'SharedBackendEnvironment', + $$type: '@backstage/SharedBackendEnvironment', version: 'v1', services: undefined, }); diff --git a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts index 2eb2cdec6a..57f0533523 100644 --- a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts +++ b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts @@ -31,7 +31,7 @@ export interface SharedBackendEnvironmentConfig { * @public */ export interface SharedBackendEnvironment { - $$type: 'SharedBackendEnvironment'; + $$type: '@backstage/SharedBackendEnvironment'; // NOTE: This type is opaque in order to allow for future API evolution without // cluttering the external API. For example we might want to add support @@ -93,7 +93,7 @@ export function createSharedEnvironment< // Here to ensure type safety in this internal implementation. const env: SharedBackendEnvironment & InternalSharedBackendEnvironment = { - $$type: 'SharedBackendEnvironment', + $$type: '@backstage/SharedBackendEnvironment', version: 'v1', services, }; diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts index d281335a8e..0326080b11 100644 --- a/packages/backend-plugin-api/src/wiring/factories.ts +++ b/packages/backend-plugin-api/src/wiring/factories.ts @@ -55,7 +55,7 @@ export function createExtensionPoint( toString() { return `extensionPoint{${config.id}}`; }, - $$ref: 'extension-point', // TODO: declare + $$type: '@backstage/ExtensionPoint', }; } diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index 6e081f57e5..ab8ce07284 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -32,7 +32,7 @@ export type ExtensionPoint = { toString(): string; - $$ref: 'extension-point'; + $$type: '@backstage/ExtensionPoint'; }; /** From 0d0cb65e5e4ae7cde89a444a236433abf099d22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C5=A0neberger?= Date: Mon, 6 Feb 2023 19:08:21 +0100 Subject: [PATCH 81/90] Release this change as a patch version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marek Šneberger --- .changeset/hip-chairs-double.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/hip-chairs-double.md b/.changeset/hip-chairs-double.md index 722795d80d..52cdde3588 100644 --- a/.changeset/hip-chairs-double.md +++ b/.changeset/hip-chairs-double.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-tech-radar': minor +'@backstage/plugin-tech-radar': patch --- Deprecate `RadarEntry.url` - use `RadarEntry.links` instead From 5b9a1e7dc00a25f474162b73ba606673df23eefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C5=A0neberger?= Date: Mon, 6 Feb 2023 19:16:04 +0100 Subject: [PATCH 82/90] Make RadarEntry.url optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marek Šneberger --- plugins/tech-radar/api-report.md | 2 +- plugins/tech-radar/src/api.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/tech-radar/api-report.md b/plugins/tech-radar/api-report.md index 2836e04461..3b84990d85 100644 --- a/plugins/tech-radar/api-report.md +++ b/plugins/tech-radar/api-report.md @@ -26,7 +26,7 @@ export interface RadarEntry { timeline: Array; title: string; // @deprecated - url: string; + url?: string; } // @public diff --git a/plugins/tech-radar/src/api.ts b/plugins/tech-radar/src/api.ts index bf90cf0750..c0b8fb8852 100644 --- a/plugins/tech-radar/src/api.ts +++ b/plugins/tech-radar/src/api.ts @@ -136,7 +136,7 @@ export interface RadarEntry { * * @deprecated Use {@link RadarEntry.links} instead */ - url: string; + url?: string; /** * History of the Entry moving through {@link RadarRing} */ From 8060b73e5ed74f053289df62717264f2fd588564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C5=A0neberger?= Date: Mon, 6 Feb 2023 19:16:27 +0100 Subject: [PATCH 83/90] Remove deprecated url attribute from sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marek Šneberger --- plugins/tech-radar/src/sample.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugins/tech-radar/src/sample.ts b/plugins/tech-radar/src/sample.ts index 0dc647ac54..1a6f372f64 100644 --- a/plugins/tech-radar/src/sample.ts +++ b/plugins/tech-radar/src/sample.ts @@ -45,7 +45,6 @@ entries.push({ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', }, ], - url: '#', key: 'javascript', id: 'javascript', title: 'JavaScript', @@ -73,7 +72,6 @@ entries.push({ 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat', }, ], - url: '#', key: 'typescript', id: 'typescript', title: 'TypeScript', @@ -91,7 +89,6 @@ entries.push({ 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur', }, ], - url: '#', links: [ { url: 'https://webpack.js.org/', @@ -111,7 +108,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', links: [ { url: 'https://reactjs.org/', @@ -131,7 +127,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'code-reviews', id: 'code-reviews', title: 'Code Reviews', @@ -145,7 +140,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'mob-programming', id: 'mob-programming', title: 'Mob Programming', @@ -159,7 +153,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'docs-like-code', id: 'docs-like-code', title: 'Docs-like-code', @@ -172,7 +165,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'force-push', id: 'force-push', title: 'Force push to master', @@ -186,7 +178,6 @@ entries.push({ description: 'long description', }, ], - url: '#', links: [ { url: 'https://reactjs.org/', From 8784b505af2ccef161a479ce3b099468e8c45a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C5=A0neberger?= Date: Mon, 6 Feb 2023 19:17:51 +0100 Subject: [PATCH 84/90] Remove url attribute from the diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marek Šneberger --- .changeset/hip-chairs-double.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/hip-chairs-double.md b/.changeset/hip-chairs-double.md index 52cdde3588..e2b03f2e66 100644 --- a/.changeset/hip-chairs-double.md +++ b/.changeset/hip-chairs-double.md @@ -6,7 +6,6 @@ Deprecate `RadarEntry.url` - use `RadarEntry.links` instead ```diff - url: 'https://www.javascript.com/', -+ url: '#', key: 'javascript', id: 'javascript', title: 'JavaScript', From 1cea9544d15fd6061339713db8f1b43c8124fd7e Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Mon, 6 Feb 2023 19:44:31 +0100 Subject: [PATCH 85/90] Update silly-buckets-joke.md Signed-off-by: Ben Lambert --- .changeset/silly-buckets-joke.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silly-buckets-joke.md b/.changeset/silly-buckets-joke.md index bd77f2ed05..113272c965 100644 --- a/.changeset/silly-buckets-joke.md +++ b/.changeset/silly-buckets-joke.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend-module-msgraph': patch --- -Fixed bug in readProviderConfig as it wasn't reading the user.select field expected from the app-config.yaml configuration +Fixed a bug reading the `user.select` field expected from the `app-config.yaml` configuration From 67ebd7f8a576638d34d37e184efc0abd6a2cfb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C5=A0neberger?= Date: Mon, 6 Feb 2023 19:45:14 +0100 Subject: [PATCH 86/90] Fix URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marek Šneberger --- plugins/tech-radar/src/sample.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tech-radar/src/sample.ts b/plugins/tech-radar/src/sample.ts index 1a6f372f64..395b4f2ff5 100644 --- a/plugins/tech-radar/src/sample.ts +++ b/plugins/tech-radar/src/sample.ts @@ -180,7 +180,7 @@ entries.push({ ], links: [ { - url: 'https://reactjs.org/', + url: 'https://github.com', title: 'Learn more', }, ], From 95433b19875cf3070349bbaf8829ae1e160623c4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Feb 2023 19:55:21 +0100 Subject: [PATCH 87/90] Apply suggestions from code review Co-authored-by: Ben Lambert Signed-off-by: Patrik Oldsberg --- .changeset/little-jars-lay.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.changeset/little-jars-lay.md b/.changeset/little-jars-lay.md index 161dc099e9..f8851f7adc 100644 --- a/.changeset/little-jars-lay.md +++ b/.changeset/little-jars-lay.md @@ -1,10 +1,6 @@ --- -'@internal/plugin-todo-list-backend': patch '@backstage/plugin-user-settings-backend': patch -'@backstage/backend-test-utils': patch '@backstage/plugin-scaffolder-backend': patch -'@backstage/backend-defaults': patch -'@backstage/backend-app-api': patch '@backstage/plugin-periskop-backend': patch '@backstage/backend-common': patch '@backstage/plugin-catalog-backend': patch From c1a19d2647b391e5565e442bf3f6dc89cf0a8164 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 21:05:07 +0100 Subject: [PATCH 88/90] chore: fix api-reports Signed-off-by: blam --- packages/backend-plugin-api/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index baa2b1b7dc..a8174c042c 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -508,7 +508,7 @@ export interface ServiceRefConfig { // @public export interface SharedBackendEnvironment { // (undocumented) - $$type: 'SharedBackendEnvironment'; + $$type: '@backstage/SharedBackendEnvironment'; } // @public From 65ec06e9e7b76080a48b5e19d78688eb0edf0cdb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 20:31:49 +0000 Subject: [PATCH 89/90] chore(deps): update dependency esbuild to v0.17.6 Signed-off-by: Renovate Bot --- yarn.lock | 182 +++++++++++++++++++++++++++--------------------------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/yarn.lock b/yarn.lock index c9caa42b95..de12cf4659 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9231,9 +9231,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/android-arm64@npm:0.17.5" +"@esbuild/android-arm64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/android-arm64@npm:0.17.6" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -9245,65 +9245,65 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/android-arm@npm:0.17.5" +"@esbuild/android-arm@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/android-arm@npm:0.17.6" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/android-x64@npm:0.17.5" +"@esbuild/android-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/android-x64@npm:0.17.6" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/darwin-arm64@npm:0.17.5" +"@esbuild/darwin-arm64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/darwin-arm64@npm:0.17.6" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/darwin-x64@npm:0.17.5" +"@esbuild/darwin-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/darwin-x64@npm:0.17.6" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/freebsd-arm64@npm:0.17.5" +"@esbuild/freebsd-arm64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/freebsd-arm64@npm:0.17.6" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/freebsd-x64@npm:0.17.5" +"@esbuild/freebsd-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/freebsd-x64@npm:0.17.6" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-arm64@npm:0.17.5" +"@esbuild/linux-arm64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-arm64@npm:0.17.6" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-arm@npm:0.17.5" +"@esbuild/linux-arm@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-arm@npm:0.17.6" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-ia32@npm:0.17.5" +"@esbuild/linux-ia32@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-ia32@npm:0.17.6" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -9315,86 +9315,86 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-loong64@npm:0.17.5" +"@esbuild/linux-loong64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-loong64@npm:0.17.6" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-mips64el@npm:0.17.5" +"@esbuild/linux-mips64el@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-mips64el@npm:0.17.6" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-ppc64@npm:0.17.5" +"@esbuild/linux-ppc64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-ppc64@npm:0.17.6" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-riscv64@npm:0.17.5" +"@esbuild/linux-riscv64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-riscv64@npm:0.17.6" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-s390x@npm:0.17.5" +"@esbuild/linux-s390x@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-s390x@npm:0.17.6" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-x64@npm:0.17.5" +"@esbuild/linux-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-x64@npm:0.17.6" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/netbsd-x64@npm:0.17.5" +"@esbuild/netbsd-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/netbsd-x64@npm:0.17.6" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/openbsd-x64@npm:0.17.5" +"@esbuild/openbsd-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/openbsd-x64@npm:0.17.6" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/sunos-x64@npm:0.17.5" +"@esbuild/sunos-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/sunos-x64@npm:0.17.6" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/win32-arm64@npm:0.17.5" +"@esbuild/win32-arm64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/win32-arm64@npm:0.17.6" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/win32-ia32@npm:0.17.5" +"@esbuild/win32-ia32@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/win32-ia32@npm:0.17.6" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/win32-x64@npm:0.17.5" +"@esbuild/win32-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/win32-x64@npm:0.17.6" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -21470,31 +21470,31 @@ __metadata: linkType: hard "esbuild@npm:^0.17.0": - version: 0.17.5 - resolution: "esbuild@npm:0.17.5" + version: 0.17.6 + resolution: "esbuild@npm:0.17.6" dependencies: - "@esbuild/android-arm": 0.17.5 - "@esbuild/android-arm64": 0.17.5 - "@esbuild/android-x64": 0.17.5 - "@esbuild/darwin-arm64": 0.17.5 - "@esbuild/darwin-x64": 0.17.5 - "@esbuild/freebsd-arm64": 0.17.5 - "@esbuild/freebsd-x64": 0.17.5 - "@esbuild/linux-arm": 0.17.5 - "@esbuild/linux-arm64": 0.17.5 - "@esbuild/linux-ia32": 0.17.5 - "@esbuild/linux-loong64": 0.17.5 - "@esbuild/linux-mips64el": 0.17.5 - "@esbuild/linux-ppc64": 0.17.5 - "@esbuild/linux-riscv64": 0.17.5 - "@esbuild/linux-s390x": 0.17.5 - "@esbuild/linux-x64": 0.17.5 - "@esbuild/netbsd-x64": 0.17.5 - "@esbuild/openbsd-x64": 0.17.5 - "@esbuild/sunos-x64": 0.17.5 - "@esbuild/win32-arm64": 0.17.5 - "@esbuild/win32-ia32": 0.17.5 - "@esbuild/win32-x64": 0.17.5 + "@esbuild/android-arm": 0.17.6 + "@esbuild/android-arm64": 0.17.6 + "@esbuild/android-x64": 0.17.6 + "@esbuild/darwin-arm64": 0.17.6 + "@esbuild/darwin-x64": 0.17.6 + "@esbuild/freebsd-arm64": 0.17.6 + "@esbuild/freebsd-x64": 0.17.6 + "@esbuild/linux-arm": 0.17.6 + "@esbuild/linux-arm64": 0.17.6 + "@esbuild/linux-ia32": 0.17.6 + "@esbuild/linux-loong64": 0.17.6 + "@esbuild/linux-mips64el": 0.17.6 + "@esbuild/linux-ppc64": 0.17.6 + "@esbuild/linux-riscv64": 0.17.6 + "@esbuild/linux-s390x": 0.17.6 + "@esbuild/linux-x64": 0.17.6 + "@esbuild/netbsd-x64": 0.17.6 + "@esbuild/openbsd-x64": 0.17.6 + "@esbuild/sunos-x64": 0.17.6 + "@esbuild/win32-arm64": 0.17.6 + "@esbuild/win32-ia32": 0.17.6 + "@esbuild/win32-x64": 0.17.6 dependenciesMeta: "@esbuild/android-arm": optional: true @@ -21542,7 +21542,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 31829b46a0cad65f58d90d39b66fd32a2de820539dd8530199975e417aa0ecd63cfc340e402f4f4e9235906a1a05969ddac1d4ad1ba611a3f11c0f62516b9390 + checksum: 13c4c3bcaa7ff291810d2c8294a1eb997b4672c60a9285b32d8eafcbc552e4468e06efe9d1a15067f5cbd41adcb12ddb4362618845e2e916e437f281c9aa80ab languageName: node linkType: hard From 546bdf30b38adbe972d39c9239d6b80333977be0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 20:33:02 +0000 Subject: [PATCH 90/90] fix(deps): update dependency @graphql-tools/schema to v9.0.16 Signed-off-by: Renovate Bot --- yarn.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index c9caa42b95..a3e3cdfcbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10125,15 +10125,15 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/merge@npm:8.3.17, @graphql-tools/merge@npm:^8.2.6": - version: 8.3.17 - resolution: "@graphql-tools/merge@npm:8.3.17" +"@graphql-tools/merge@npm:8.3.18, @graphql-tools/merge@npm:^8.2.6": + version: 8.3.18 + resolution: "@graphql-tools/merge@npm:8.3.18" dependencies: - "@graphql-tools/utils": 9.2.0 + "@graphql-tools/utils": 9.2.1 tslib: ^2.4.0 peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 943e1ad64575ab9ae6726d434cf7b08e80099442f9d25c1ac9f11fdac7bd0e8e73109f0e2469b47650ad0a0731ac850ede844b5ff8a7eab352027a42c1e78c79 + checksum: 0834fcf13dec5ee92e39f2e1b27a098654e2db20bf6ef6f43d7efce56b7d750caa59dd16aacb211b8f5a198d29c19d36471401762f5a34e0bc22657285dcae92 languageName: node linkType: hard @@ -10261,16 +10261,16 @@ __metadata: linkType: hard "@graphql-tools/schema@npm:^9.0.0": - version: 9.0.15 - resolution: "@graphql-tools/schema@npm:9.0.15" + version: 9.0.16 + resolution: "@graphql-tools/schema@npm:9.0.16" dependencies: - "@graphql-tools/merge": 8.3.17 - "@graphql-tools/utils": 9.2.0 + "@graphql-tools/merge": 8.3.18 + "@graphql-tools/utils": 9.2.1 tslib: ^2.4.0 value-or-promise: 1.0.12 peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 4361cb1a355e8811fd9b762bd137a2f8fd287b05f03cb574bc3d5e6855aeceef462e72bacfa89dd9028138e9fe3cafc4d69c95d7f9a9b6ab7f037ca3e36f52a9 + checksum: ecbd27b4a36424c8d4b2206b1f2d2ec2cff3c66f52ec452b17977642017ca7f00df4df44f2d0426174ceea9596474d0656bc0227f4c1149f2288abb6d186cd68 languageName: node linkType: hard @@ -10363,15 +10363,15 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/utils@npm:9.2.0, @graphql-tools/utils@npm:^9.0.0, @graphql-tools/utils@npm:^9.1.1": - version: 9.2.0 - resolution: "@graphql-tools/utils@npm:9.2.0" +"@graphql-tools/utils@npm:9.2.1, @graphql-tools/utils@npm:^9.0.0, @graphql-tools/utils@npm:^9.1.1": + version: 9.2.1 + resolution: "@graphql-tools/utils@npm:9.2.1" dependencies: "@graphql-typed-document-node/core": ^3.1.1 tslib: ^2.4.0 peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 9754f8c6d52490df395626cef224542f37ca040ced6688c8ad80b1e0498006186529a50ac21979465123debba093db33dbfe8f1f7a9101f969dec0f31dc9a45b + checksum: 94ed12df5f49e5c338322ffd931236a687a3d5c443bf499f9baab5d4fcd9792234111142be8aa506a01ca2e82732996c4e1d8f6159ff9cc7fdc5c97f63e55226 languageName: node linkType: hard